Skip to content
wannacode

Derived state

In short: Derived state is a value computed from the state during render (a count, a total, a filtered list) instead of being stored separately; for an expensive calculation useMemo keeps the result between renders.

Problem

The total falls behind

The cart has white bread and fresh milk. Press "+" on the bread: the count and the total grow. Now remove the milk.

React

The line disappears, the count drops to 3, and the total stays €5.20. The component keeps three states: items, count and total. Every click has to change all three, and the remove function changes two. This is the same drift as in the lesson "A list from data", only inside a component. The library draws exactly what you give it. You give it two truths about one cart.

The idea

Compute it, do not keep it

Ask about every piece of state: can I compute it from the other state or from the props? If you can, it is not state. The count is the sum of the quantities in items. The total is the sum of price times quantity. Both are computed.

Such a value is called derived state. You keep only items and compute the rest from them every time the component renders.

const [items, setItems] = useState(START);
const count = items.reduce((sum, i) => sum + i.qty, 0);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);

setItems makes React run the function App again. Then count and total are computed from the new items. They are plain constants. They need no useState and nothing special.

Now the functions more and remove change only items. There is nothing left for them to forget.

See it

See the computed values and the copy

On the left are the cart and a copy of the total, kept separately. On the right are the three computed values.

const [items, setItems] = useState(START);
const [savedTotal, setSavedTotal] = useState(26.9);
const count = items.reduce((s, i) => s + i.qty, 0);
const total = items.reduce((s, i) => s + i.price * i.qty, 0);
const free = total >= 30;
function more(item) {
setItems(items.map((i) =>
i.id === item.id ? { ...i, qty: i.qty + 1 } : i,
));
setSavedTotal(savedTotal + item.price);
}
function remove(item) {
setItems(items.filter((i) => i.id !== item.id));
}
Step by step, in words
  1. On the left is the state: the cart items. Below it is a second state, savedTotal: the same total, kept as a copy.
  2. On the right are count, total and free: plain constants. React runs App on every render, so they are computed again from items each time: €26.90, €3.10 left to free delivery.
  3. more(cheese): items change and the three arrows light up. The total is €34.80, delivery is free. more() also updates savedTotal by hand, so the copy is still right.
  4. remove(cheese): items change, and count, total and free follow by themselves: €3.20. Nobody wrote a line for savedTotal in remove().
  5. The copy says €34.80, the cart is €3.20. No error, only a wrong number: the state and its copy have drifted apart.
  6. The rule: keep only what cannot be computed. Whatever follows from the state is computed from it, so there is no copy to forget.

The computed values follow the cart on every change, because they have no memory of their own. The copy follows only as long as somebody remembers to update it. The first function that forgets makes it wrong.

Try it

Compute the count and the total

The cart keeps count and total as separate states, and remove() forgets the total. Remove both states and compute the count and the total from items. The check presses "+" on the bread, then removes the milk and looks at the count and the total.

React

Try it

Fill in

The count is computed from the cart. What goes in the blank?

const [items, setItems] = useState(START);
 count = items.reduce((sum, i) => sum + i.qty, 0);

The idea

When to keep the result

The constant is computed on every render of the component, even when items are the same and the render is because of some other state. For a total of ten lines that is nothing. If the calculation is expensive (sorting thousands of products), React has useMemo:

const sorted = useMemo(() => products.toSorted(byPrice), [products]);

React keeps the result and runs the function again only if one of the dependencies in the array ([products]) differs from the previous render. If you forget a dependency, the result stays old. So useMemo is not for every total. Start with a constant and add it when you measure that the calculation is slow. The result is the same, it is only computed less often.

The idea

The filter is computed too

The catalogue has the buttons "All", "Bread" and "Milk". The temptation is to keep two arrays: all products and the visible ones. But the visible ones follow from two other facts: the full list and the chosen category. You keep only the category:

const [category, setCategory] = useState('all');
const visible = category === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category);

Sorting is the same: you keep "by price" or "by name", and the sorted list is computed. Watch out for sort(): it sorts the array in place, which means it changes the state. toSorted() returns a new array and leaves the old one.

Try it

Only the category

The buttons change the category, but the list always shows everything. Compute the visible products from the category and show "Showing 3 of 5" above the list. The check picks "Milk", then "Bread", then "All".

React

The idea

A copy of a prop

The same mistake happens between components. App gives 20% off and passes the new prices to every ProductCard. The card, however, remembers the price it got the first time:

React

Press "20% off". The button is pressed, and the bread is still €1.20.

const [price] = useState(product.price) takes product.price only on the first render of the card. After that React renders the card with the new product, but its state is its own and stays.

The fix is not to copy: show product.price directly. If the card has to show something computed from a prop (the price with VAT, the price per unit), compute it during render.

Try it

Find the bug

The buyer presses "+": the quantity on the line becomes 3, and the line total stays as for 2. Which line is to blame?

Pick the line with the bug.

More precisely, if you are curious

When the copy is on purpose. Sometimes you want a value from outside only as a start: an edit form starts with the current name, and then the person types. Then the copy is right, but say so in the name: the prop initialName, the state name. Whoever reads the code sees that changes from outside are not followed.

useMemo is a hint. React keeps the right to throw away the remembered result and compute again, and the documentation says so explicitly. So the code has to be correct without useMemo. React Compiler, if the project uses it, remembers such calculations by itself, and a manual useMemo is needed even less often.

Renders are frequent. A component renders again when its state changes or when its parent renders. The constants in it are computed every time. That is why the body of a component has no fetch, timers or state changes: those are for effects, the next module.

The same holds for data from the server. If the server returns the products and you keep "number of products" separately, the two can drift apart after a new load. Keep the response and compute the count.

Exercise

Free delivery

The cart has "−" and "+" buttons on every line ("−" on the last unit removes the line). Below the list show "Items: 4", "Total: €18.20" and "Free delivery in another €11.80", and from €30 (FREE_FROM) only "Delivery is free". Everything is computed from items. The check adds cheese up to €34.00, then goes back under €30 and removes the bread.

React

Test

Check yourself

1. The cart keeps items. Which of these should be a separate state?
2. When should you wrap a calculation in useMemo?
3. const [price] = useState(product.price); in ProductCard. The parent passes a new price. What does the card show?
4. The list is filtered by category. What belongs in the state?

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