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.
Console
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
- On the left is the state: the cart items. Below it is a second state, savedTotal: the same total, kept as a copy.
- 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.
- 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.
- remove(cheese): items change, and count, total and free follow by themselves: €3.20. Nobody wrote a line for savedTotal in remove().
- The copy says €34.80, the cart is €3.20. No error, only a wrong number: the state and its copy have drifted apart.
- 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.
Console
Solution
App.jsximport { useState } from 'react';
import { money } from './money.js';
const START = [
{ id: 1, name: 'White bread', price: 1.2, qty: 2 },
{ id: 5, name: 'Fresh milk', price: 1.6, qty: 1 },
];
export default function App() {
const [items, setItems] = useState(START);
// Count and total: computed on every render.
const count = items.reduce((sum, i) => sum + i.qty, 0);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
function more(item) {
setItems(items.map((i) => (i.id === item.id ? { ...i, qty: i.qty + 1 } : i)));
}
function remove(item) {
setItems(items.filter((i) => i.id !== item.id));
}
return (
<main>
<h1>The cart</h1>
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name} × {item.qty}</span>
<button type="button" aria-label={`One more ${item.name}`} onClick={() => more(item)}>+</button>
<button type="button" aria-label={`Remove ${item.name}`} onClick={() => remove(item)}>Remove</button>
</li>
))}
</ul>
<p>Items: {count}</p>
<p>Total: {money(total)}</p>
</main>
);
}
money.jsexport const money = (x) => '€' + x.toFixed(2);
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button { font: inherit; }
button[aria-pressed="true"] { background: #2f6f4f; color: white; }
Only items is left. The count is items.reduce((sum, i) => sum + i.qty, 0), the total is the same with i.price * i.qty. In React these are constants in the body of App: they are computed on every render. more and remove now change only the cart.
A trap: const [total] = useState(items.reduce(…)). The initial value of useState is taken only on the first render, so the total stays €4.00 forever.
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);const: a plain constant in the body of the component. It is computed again on every render, while useState here would remember only the first value.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".
Console
Solution
App.jsximport { useState } from 'react';
import ProductCard from './ProductCard.jsx';
const PRODUCTS = [
{ id: 1, name: 'White bread', price: 1.2, category: 'bread' },
{ id: 2, name: 'Wholegrain bread', price: 2.1, category: 'bread' },
{ id: 5, name: 'Fresh milk', price: 1.6, category: 'milk' },
{ id: 6, name: 'Yoghurt', price: 0.9, category: 'milk' },
{ id: 7, name: 'White cheese', price: 7.9, category: 'milk' },
];
const CATEGORIES = [
{ id: 'all', name: 'All' },
{ id: 'bread', name: 'Bread' },
{ id: 'milk', name: 'Milk' },
];
export default function App() {
const [category, setCategory] = useState('all');
const visible = category === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category);
return (
<main>
<h1>Products</h1>
{CATEGORIES.map((c) => (
<button key={c.id} type="button" aria-pressed={category === c.id} onClick={() => setCategory(c.id)}>
{c.name}
</button>
))}
<p>Showing {visible.length} of {PRODUCTS.length}</p>
<ul>
{visible.map((p) => <ProductCard key={p.id} product={p} />)}
</ul>
</main>
);
}
ProductCard.jsximport { money } from './money.js';
export default function ProductCard({ product }) {
return (
<li>
<span>{product.name}</span> {money(product.price)}
</li>
);
}
money.jsexport const money = (x) => '€' + x.toFixed(2);
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button { font: inherit; }
button[aria-pressed="true"] { background: #2f6f4f; color: white; }
visible is computed from category and PRODUCTS: for "All" it is the whole list, otherwise a filter by category. The count is visible.length of PRODUCTS.length.
If visible is a second state, the code often turns into this: setCategory(id) and right after it setVisible(… category …). But category in this click is still the old value: the new one comes with the next render. The list is one click behind. With a computed value this problem does not exist.
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:
Console
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.
item.price * item.qty into state. useState takes the initial value once, so the line total does not follow the quantity. It should be const subtotal = item.price * item.qty;.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.
Console
Solution
App.jsximport { useState } from 'react';
import { money } from './money.js';
const START = [
{ id: 7, name: 'White cheese', price: 7.9, qty: 2 },
{ id: 1, name: 'White bread', price: 1.2, qty: 2 },
];
const FREE_FROM = 30;
export default function App() {
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);
const free = total >= FREE_FROM;
function more(item) {
setItems(items.map((i) => (i.id === item.id ? { ...i, qty: i.qty + 1 } : i)));
}
function less(item) {
if (item.qty === 1) setItems(items.filter((i) => i.id !== item.id));
else setItems(items.map((i) => (i.id === item.id ? { ...i, qty: i.qty - 1 } : i)));
}
return (
<main>
<h1>The cart</h1>
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name} × {item.qty}</span>
<button type="button" aria-label={`One less ${item.name}`} onClick={() => less(item)}>−</button>
<button type="button" aria-label={`One more ${item.name}`} onClick={() => more(item)}>+</button>
</li>
))}
</ul>
<p>Items: {count}</p>
<p>Total: {money(total)}</p>
{free ? <p>Delivery is free</p> : <p>Free delivery in another {money(FREE_FROM - total)}</p>}
</main>
);
}
money.jsexport const money = (x) => '€' + x.toFixed(2);
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button { font: inherit; }
button[aria-pressed="true"] { background: #2f6f4f; color: white; }
The count, the total and free are computed from items; the delivery message is conditional rendering: free or the remaining amount FREE_FROM - total. The check goes above €30 and back under it. That is why "free" kept as a separate state is caught: it becomes true on "+", but nobody sets it back on "−". For the same reason the check also catches useMemo(…, []): the empty array says "never compute again".
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Lists and keys · Conditions: empty, error, loading · State · Updating objects and arrays
- Continues in: Lifting state up · Effects · Shared state · Routing
- Tasks on this topic: State