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.
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:
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:
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
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:
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
Any state that's a pure function of other state is mirror state. Delete it and compute it.
The Stale Derivation
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
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(n²) 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.