Skip to content
React Lab · LAB-04lifting state / colocation

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 laboratory

Move 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

  1. 01Find every component that reads the value. Put the state at their lowest common ancestor.
  2. 02If only one component reads it, it belongs inside that component.
  3. 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.
  4. 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.
  5. 05If a parent must hold the state but the subtree does not depend on it, pass the subtree as children.
example.jsxjsx
// 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;
Derived state is a bug generator

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

  1. 01React docsChoosing the state structure
  2. 02React docsYou might not need an effect
  3. 03React docsuseTransition