Skip to content
wannacode

Lifting state up

In short: Lifting state up moves shared state to the closest common parent: data come down as props, and changes go up through a callback.

Problem

The badge at the top does not know

The shop has a cart badge at the top and a list of products below. Add bread, milk and one more bread.

React

The bread card says "In the cart: 2", and the top says "Cart (0)". The cart lives in ProductList: that is where the button is pressed and where the state was added. CartBadge is another component, a sibling of ProductList, and it has its own number that nobody changes. The two components have two carts, and each is right about its own.

The idea

Up to the common parent

A component sees only its own state and the props its parent passes to it. A sibling cannot see its sibling. So two places that show one cart cannot each have their own.

The solution is to move the state to the closest common parent of the two. Here that is App. This is called lifting state up:

  1. App keeps items and computes count.
  2. Data come down as props: count to CartBadge, items to ProductList.
  3. Changes go up as a message: the button in ProductCard does not change the cart, it tells App "add this product".
export default function App() {
const [items, setItems] = useState([]);
const count = items.reduce((sum, i) => sum + i.qty, 0);
function add(product) { /* changes items */ }
return (
<>
<header><CartBadge count={count} /></header>
<main><ProductList products={PRODUCTS} items={items} onAdd={add} /></main>
</>
);
}

onAdd={add} passes the function down. The child calls it when something happens: onClick={() => onAdd(product)}. Such a function is called a callback.

Now CartBadge and ProductList have no state of their own for the cart. They show what App gives them.

See it

See it move up

At first the cart is in ProductList. Then it moves to App and flows down from there.

function App() {
const [items, setItems] = useState([]);
const count = items.reduce((s, i) => s + i.qty, 0);
function add(product) {
setItems(plusOne(items, product));
}
return (
<>
<CartBadge count={count} />
<ProductList items={items} onAdd={add} />
</>
);
}
function ProductList({ items, onAdd }) {
return PRODUCTS.map((p) => (
<ProductCard key={p.id} product={p}
qty={qtyOf(items, p)} onAdd={onAdd} />
));
}
function ProductCard({ product, qty, onAdd }) {
return <button onClick={() => onAdd(product)}>…</button>;
}
Step by step, in words
  1. CartBadge and ProductList are siblings: both are children of App. At first the cart lives in ProductList, and CartBadge has its own count.
  2. A click on "Add" reaches ProductList, and the list counts 1. CartBadge cannot see that state: a sibling cannot read another sibling. It stays at 0.
  3. The fix: move the cart to the closest common parent of the two, App. The badge drops its own copy.
  4. From App the data flow down as props: count to CartBadge, items to ProductList and qty to every ProductCard. Both siblings now read one cart.
  5. The child does not own the cart, so it cannot change it. The click calls onAdd(product): a function that App passed down. The call travels up to add() in App.
  6. App changes items, and the new props flow down: the badge says 2, the card says 2. Nobody updated them by hand.
  7. The rule: the state lives in one place, as high as needed and no higher. Data go down as props, changes go up as events.

The direction is the same in every app: data down, messages up. Whoever owns the data is the only one who changes them.

Try it

Move the cart up

The cart is in ProductList, and CartBadge counts a number of its own. Move the cart to App: CartBadge gets the count as a prop, ProductList gets items and reports "Add" up. The check adds bread, milk and bread and looks at the cards and the badge.

React

The idea

The child reports, the parent decides

ProductCard does not know how the cart is built. It only knows that someone pressed "Add" on its product. So it reports what happened, not what to change: "add this product", not "increase qty on line 2". App decides what that means: a new line or one more unit.

This keeps the card simpler, and it can sit anywhere: in the catalogue, in "Recommended", in the search. It does not own the data and does not touch them.

The names follow a convention: a prop with a function starts with on and names the event: onAdd, onRemove, onChange. The function in the parent is often add or handleAdd. You pass the function, not its result: onClick={() => onAdd(product)}, not onClick={onAdd(product)}.

Try it

Fill in

What goes in the blank so that the click reaches the parent?

export default function ProductCard({ product, onAdd }) {
  return <button onClick={() => (product)}>Add</button>;
}

Try it

Find the bug

The page opens and the cart badge grows by itself, without a click. Which line is to blame?

Pick the line with the bug.

The idea

When the tree gets deep

How far up? As high as needed, and no higher. If only ProductCard uses something (whether the description is open), it stays in it. You move it up when a second component has to see it.

In a real shop the cart is needed in many places: the badge at the top, the list, the product page, the order. The common parent is high up, and the way to them passes through components that do not use the cart, they only pass it down. This is called prop drilling. With two levels it is normal. With five levels every component on the way has to know about data that are not its own.

There are separate tools for this: Context. They give the data to distant descendants without the components on the way. They are in the lesson on shared state in the module "Effects, data and forms". The rule stays the same: the data have one owner, and changes go through it.

More precisely, if you are curious

Props are read-only. In development mode React freezes the props object: props.qty = 3 throws TypeError: Cannot assign to read only property 'qty'. A child that wants a change reports up.

Why an arrow. onClick wants a function for React to call later. onClick={add} is a function too, but React calls it with the click event, not with the product. So onClick={() => onAdd(product)}: a new function that knows the product.

Class components have the same model: the parent passes onAdd as a prop and the child calls this.props.onAdd(product).

A copy is not lifting. If the parent holds the cart and the child makes its own copy with useState(items), the copy takes only the first value and stops following the parent. That is the mistake from the lesson on derived state.

Exercise

The cart on the side

Next to the list there is a third part, CartSummary: the heading "The cart", lines like "White bread × 2" with a "Remove" button, "Total: €4.00" and "The cart is empty" when it is empty. Right now CartSummary keeps a cart of its own that nobody fills. Give it the cart from App and make "Remove" remove from it. The check adds three products, removes them and looks at the badge, the cards and the total.

React

Test

Check yourself

1. CartBadge is in the header, ProductList is in the main part. Both are children of App. Where should the cart be?
2. ProductCard says onClick={onAdd(product)}. What happens?
3. Why does ProductCard not change the cart itself, but report up?
4. The cart is needed by a component five levels below App. The three components on the way only pass it down. What is this called?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·