Skip to content
React Lab · LAB-04render phase / commit phase

Why did this component render?

Nothing it displays changed. It rendered anyway. Four causes, and only one of them is a bug.

Level
DEVELOPER
Read time
10 min
Experiment
Available
Type
SIMULATION

The question

A leaf component re-renders on every keystroke in an unrelated input. Its props look identical. What actually triggers a render?

Hypothesis

A component renders when its own state changes, when its context value changes, or when its parent renders, and by default, the last one does not care whether props changed.

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

Change state at different points in the tree and watch the render wave travel. Toggle memo and stable references to see where the wave stops.

What we observed

TriggerWhat re-renders
setState in a componentThat component and its entire subtree
Parent re-rendersAll children, memoised or not, unless memo + equal props
Context value changesEvery consumer of that context, wherever it sits
New key on an elementOld instance unmounts, new one mounts with fresh state
Ref changesNothing, refs do not trigger renders

Why it happens

The usual culprit is a new reference created during render. Objects, arrays, inline functions and JSX elements are fresh values on every call, so a memo'd child that receives one is never actually memoised.

example.jsxjsx
// New object every render<Chart options={{ animate: true }} />// New function every render<Row onSelect={() => select(row.id)} />// New array every render<List items={data.filter(Boolean)} />// Stable alternativesconst options = useMemo(() => ({ animate: true }), []);const onSelect = useCallback((id) => select(id), [select]);const items = useMemo(() => data.filter(Boolean), [data]);
Three references that defeat memo

The structural fix is usually better than the memo fix. If the state that changes on every keystroke lives above a large subtree, moving that state *down* into the component that uses it removes the render wave entirely, no memo, no dependency arrays, no stale-closure bugs.

example.jsxjsx
// Every keystroke re-renders <ExpensiveTree />function Page() { const [q, setQ] = useState(""); return ( <> <input value={q} onChange={(e) => setQ(e.target.value)} /> <ExpensiveTree /> </> );}// The subtree is passed as children, so it is not re-createdfunction Page() { return ( <SearchBox> <ExpensiveTree /> </SearchBox> );}
Composition beats memoisation
The Why MachineDEPTH 1 / 4

Why does React re-render children by default?

  1. Because it cannot know whether a child's output depends on something it cannot see.

Further research

In development, React's StrictMode intentionally double-invokes component bodies, initialisers and effects to surface impure renders. It does not do this in production. A component that behaves differently under StrictMode has a side effect in its render path.

References

  1. 01React docsRender and commit
  2. 02React docsmemo
  3. 03React docsReact Compiler