State placement experiment
The same feature, built three times. Only the location of one `useState` changes. The performance profile does not survive it.
- Level
- DEVELOPER
- Read time
- 08 min
- Experiment
- Available
- Type
- INTERACTIVE
The question
Where should state live: at the top for convenience, or as low as possible for performance?
Hypothesis
State should live at the lowest common ancestor of everything that reads it. Higher costs unnecessary renders; lower forces synchronisation bugs.
Method
Laboratory available
The instrument for this investigation runs in the laboratory, where the controls, the live model and the observation log share one workstation.
Enter the laboratoryMove the state marker up and down the tree. The bench highlights which components re-render on each update and counts the work.
What we observed
Every level you lift state costs you the render of the entire subtree below the new owner. Every level you push it down removes components from the render wave, often without any memoisation at all.
Why it happens
Placement rules that hold up in practice
- 01Find every component that reads the value. Put the state at their lowest common ancestor.
- 02If only one component reads it, it belongs inside that component.
- 03Do not store what you can compute: derived values belong in the render body, not in a second useState kept in sync by an effect.
- 04If the value is genuinely global (theme, session), context is correct, but split contexts so a fast-changing value does not re-render consumers of a slow-changing one.
- 05If a parent must hold the state but the subtree does not depend on it, pass the subtree as
children.
// Two sources of truth, kept in sync by handconst [items, setItems] = useState([]);const [count, setCount] = useState(0);useEffect(() => setCount(items.length), [items]); // extra render, can desync// One source of truthconst [items, setItems] = useState([]);const count = items.length;Further research
React 18's automatic batching groups every setState in the same event, including inside promises, timeouts and native handlers, into a single render. useTransition goes further: it marks an update as non-urgent so React can keep the input responsive and interrupt the expensive render if a keystroke arrives.
References
- 01React docsChoosing the state structure
- 02React docsYou might not need an effect
- 03React docsuseTransition