awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

Bailout, memoisation, and tearing

Crux React bails out of re-running a component if props and state are unchanged by reference. Referential stability (useMemo, useCallback, React.memo) is what enables the bailout. useSyncExternalStore prevents concurrent-render tearing.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 16 min

A component wrapped in React.memo still re-renders on every parent render. Nothing changed. The memo is there. Open the DevTools Profiler and the reason says “props changed” — but the prop is an object whose contents are identical. The problem is not the memo. The problem is an inline object literal passed as a prop: a fresh reference every render, and Object.is always reports a change.

Bailout: how React skips work. Not every component in a re-rendering subtree actually re-runs. When React reaches a fiber during render, it checks: did props change (by Object.is on each prop)? did state change? is there a pending context update this fiber consumes? If none of these is true, React bails out — it clones the fiber from the alternate, reuses the entire existing child subtree by reference, and does not call the component function at all.

This is why referential stability matters so much: a child wrapped in React.memo bails out only if its props are referentially equal. Pass it an inline {} or an arrow function as a prop and the bailout fails every render, because a fresh literal is never Object.is-equal to the last one. useMemo and useCallback exist to preserve those references so the bailout can fire.

The mental model: re-rendering a parent does not automatically re-render children — it offers them the chance to bail out, and stable references are what let them take it.

Bailout decision per fiber
Props equal (Object.is) AND state unchanged AND no context update → bail out: clone fiber, reuse child subtree, skip component call
Any prop differs by reference OR state changed OR context updated → re-run component function, diff new element tree
Common trap: inline {} or () => {} as prop → new reference every render → bailout never fires even with React.memo

State is stored on the fiber, not the component. A function component has no instance — it is just a function React calls. So where does useState keep its value across renders? On the fiber. Each fiber holds a linked list of hook records, one per useState/useReducer/useEffect call, in call order. React advances a cursor through that list as your component calls hooks.

This is the deep reason for the Rules of Hooks: hooks must be called unconditionally and in the same order every render, because React identifies each hook purely by its position in the call sequence, not by any name. Put a useState behind an if and on the render where the condition flips, every subsequent hook reads the wrong record. It also explains state preservation: state survives a re-render because the fiber survives; state is destroyed when the fiber is destroyed — which happens on unmount, on a type change, or on a key change.

Why this works

Why not name hooks? Hook records could be stored by name instead of position, eliminating the ordering requirement. But names would need to be unique per component, turning every hook call into a string-keyed lookup instead of a cursor advance. Position-based lookup is O(1) per hook and requires no extra runtime. The tradeoff: mandatory call order in exchange for minimal overhead per render.

Tearing and useSyncExternalStore. Interruptible rendering introduces a hazard called tearing: if an external store changes between two time-slices, the first slice’s components saw the old value and the later slice’s components see the new one, so the committed UI is internally inconsistent — “torn”. React’s own state cannot tear because React controls when it changes; external stores can, because React does not.

useSyncExternalStore fixes this: it gives React a subscribe function and a getSnapshot function. React reads the snapshot, and if it detects the snapshot changed during the concurrent render, it restarts the render, guaranteeing every component in one commit observed the same store value. Any state library integrating with React 18 (Redux, Zustand, Jotai) routes through useSyncExternalStore for this reason.

Trace it
1/4

A dashboard re-renders slowly on every keystroke in an unrelated search box. The React Profiler shows almost every component on the page re-rendering, even ones whose props did not change. Where is the problem?

1
Step 1 of 4
A context provider near the top of the tree has a value that is a fresh object literal every render — every consumer re-renders because the context value is a new reference
2
Locked
The search box is missing a debounce
3
Locked
React's diffing algorithm is O(n³)
4
Locked
The list needs virtualization
Quiz

A component wrapped in `React.memo` still re-renders every time its parent does. Its props look unchanged. What is the most likely cause?

Quiz

Why can you not read the updated state value synchronously on the line right after calling `setState`?

Which RFC?

Which React API exists specifically to prevent 'tearing' — components in one commit observing different values of an external store during a concurrent render?

Recall before you leave
  1. 01
    A component is wrapped in React.memo but re-renders on every parent render. Walk through diagnosing and fixing it.
  2. 02
    Why must hooks be called in the same order every render?
  3. 03
    What is tearing, and how does useSyncExternalStore prevent it?
Recap

React bails out of re-running a component when Object.is finds all its props and state unchanged — it clones the fiber and skips the function call entirely. React.memo makes the bailout conditional on props; useMemo and useCallback preserve the stable references that let the bailout fire. Hooks are stored as a positional linked list on the fiber, which is why call order must be unconditional — skipping a hook misaligns the cursor for every subsequent one. State lives on the fiber, not the component function, which is why state persists across re-renders and disappears on unmount, type change, or key change. In concurrent mode, external stores can cause tearing — components in one commit seeing different values — because a store can mutate between render slices. useSyncExternalStore detects this and restarts the render, which is why every serious React state library routes through it.

Connected lessons
appears again in143
Continue the climb ↑React Profiler, the Compiler, and production observability
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.