Derived State

State Management// 5 min read

Compute values from existing state instead of storing them separately — eliminate an entire class of sync bugs.

The Problem

A shopping cart component that stores items, totalPrice, itemCount, hasDiscount, and discountedPrice in state. Five pieces of state, but only one is source-of-truth — the items array. The other four are derivable from it.

Now someone adds an item but forgets to update itemCount. Or applies a discount but doesn't recalculate discountedPrice. Or removes an item and updates the count but not the total. Each “sync bug” is trivial to fix. The problem is they keep appearing, because the architecture makes them possible.

TS
1// Every update must touch multiple state fields in sync
2function addItem(item: CartItem) {
3 setItems(prev => [...prev, item]);
4 setItemCount(prev => prev + 1);
5 setTotalPrice(prev => prev + item.price);
6 if (item.category === "sale") {
7 setHasDiscount(true);
8 setDiscountedPrice(prev => recalculate(/* ... */));
9 }
10}

The more state fields you store, the more update paths you need, and the more opportunities for them to drift out of sync. This is a combinatorial explosion hiding behind innocent-looking useState calls.

The Principle

If a value can be computed from other state, compute it — don't store it.

Derived state is not state at all. It's a pure function of state. The cart example becomes:

TS
1const [items, setItems] = useState<CartItem[]>([]);
2
3// Derived — computed on every render, always in sync
4const itemCount = items.length;
5const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
6const hasDiscount = items.some(item => item.category === "sale");
7const discountedPrice = hasDiscount
8 ? totalPrice * 0.9
9 : totalPrice;

Now there's one update path: change items. Everything else follows. The sync bug is structurally impossible because there's nothing to sync.

The cost is computation on every render. For most cases, this is negligible — Array.reduce over 50 cart items takes microseconds. When it's not negligible, useMemo is the escape hatch:

TS
1const totalPrice = useMemo(
2 () => items.reduce((sum, item) => sum + item.price, 0),
3 [items]
4);

But reach for useMemo only after profiling. The default should be raw computation. useMemo is a performance optimization, not a correctness tool — derived state is correct with or without it.

Derived State

items[🍎]
itemCount1
totalPrice$3
hasDiscountfalse
 
mode

Stored mode — every value managed with separate setState

Real-World Example

Zustand's selector pattern takes derived state one step further. Instead of computing inside the component, you derive at the subscription level — the component only re-renders when its specific derived value changes:

TS
1// Store has raw state
2const useCartStore = create((set) => ({
3 items: [],
4 addItem: (item) => set((s) => ({ items: [...s.items, item] })),
5 removeItem: (id) => set((s) => ({ items: s.items.filter(i => i.id !== id) })),
6}));
7
8// Components subscribe to derived slices
9function CartBadge() {
10 // Only re-renders when count actually changes
11 const count = useCartStore((s) => s.items.length);
12 return <span>{count}</span>;
13}
14
15function CartTotal() {
16 // Only re-renders when total actually changes
17 const total = useCartStore((s) =>
18 s.items.reduce((sum, item) => sum + item.price, 0)
19 );
20 return <span>${total.toFixed(2)}</span>;
21}

The selector is the derivation. Zustand's useStore(selector) uses referential equality to skip re-renders when the derived value hasn't changed. This is derived state + subscription slicing — two principles working together.

Anti-Patterns

The Mirror State

TS
1const [items, setItems] = useState([]);
2const [itemCount, setItemCount] = useState(0);
3// itemCount is ALWAYS items.length — why store it?

Any state that's a pure function of other state is mirror state. Delete it and compute it.

The Stale Derivation

TS
1const [items, setItems] = useState([]);
2const [total, setTotal] = useState(0);
3
4useEffect(() => {
5 setTotal(items.reduce((s, i) => s + i.price, 0));
6}, [items]);

Using useEffect to sync derived state introduces a render where total is stale (the render before the effect fires). It also adds unnecessary complexity — the effect is doing what a plain variable assignment already does.

The Over-Memoized Derivation

TS
1const hasItems = useMemo(() => items.length > 0, [items]);

items.length > 0 takes nanoseconds. The useMemo overhead (dependency comparison, closure allocation) likely costs more than the computation it's “optimizing.”

Related Principles

  • Primitive Composition — The geometry layer in a primitive composition stack is derived state: pure functions that compute positions from raw data. Same principle, different scale.
  • Compound Components — Compound components share source-of-truth state via context. Sub-components derive their own view of that state (e.g., “am I the selected item?”) rather than storing it.

When to Break This Rule

  • Expensive computations that don't change often. If deriving a value requires O() work and the source state changes on every keystroke but the derived value only changes on submit, memoization (or even caching in state) is justified. Profile first.
  • Values from external sources. A value fetched from an API isn't derived from client state — it's external state. Store it.
  • Undo/redo history. If you need to traverse previous states, you need to store snapshots. Derived values can't be “un-derived” to recover past state.

Deep-Dive References

  • React State Without Re-render, Post 2 — How Zustand uses selectors as derived state with subscription slicing to eliminate unnecessary re-renders.

Related Principles