Part 11 · 5 chapters · ~30 min

Anti-Patterns At Scale

The recurring React bugs are a short list, and each one is a mechanism from the earlier parts applied where another was needed. This part is effect chains frame by frame, derived state and copied props, prop drilling and context overuse with the composition fix, premature memo and the volume problems memo cannot fix, and a catalogue by symptom with the Profiler signature of each.

54

Effect chains

the question

"Selecting a country flickers through three wrong states before settling. The code is four small useEffects. What is wrong with small effects?"

Nothing is wrong with small effects that synchronise with the outside world. These four synchronise state with other state, and each one runs after a paint the user has already seen. One action becomes four renders and three frames of inconsistent UI, and the logic that relates country to states to selection to estimate is spread across four places with dependency arrays as the only documentation. The fix is to compute the derived values during render (one render, consistent by construction) and to do action-driven work in the handler.

recognising one
  1. An effect whose body is only setState of something computable from its dependencies: a derivation. Replace with a variable in render, useMemo if expensive.
  2. An effect that responds to a user action ("when the selection changes, send analytics / reset the form / open the panel"): a handler. Do it where the action happened.
  3. An effect that resets state when a prop changes: a key. Remount with key={prop} and the state starts fresh with no wrong frame.
  4. An effect that adjusts state when another state changes: either derive, or use the "set state during render" form (if (a !== prevA) { setPrevA(a); setB(…) }), which React re-renders before commit, so no frame is wrong.
  5. An effect that truly synchronises (a subscription, a fetch, a DOM measurement, a document title): keep it, name it, give it a cleanup.
the cost, in numbers
  1. Each link in the chain is a render, a commit, a paint, and a passive-effects task: 10 to 30 ms on a phone. A four-link chain is a visible stutter and three wrong frames.
  2. Each link runs on a Default lane after the sync render, so an interaction's INP (the browser course part 12) includes the chain if the user is waiting on the final state.
  3. The Profiler shows it as several commits per interaction, each "hooks changed" on the same component.
AN EFFECT CHAIN
four renders and three frames to do one thing
swipe the figure sideways, or tap expand for full screen
1/6
render 1
The user picks "Nigeria". onChange: setCountry("NG"). Render 1 (sync): the country is NG; the states list is still the old array (for GH); the selected state is "Accra"; the estimate is the old one. Commit; paint: frame 1 shows Nigeria with Ghanaian states.
55

Derived state in state, and copied props

code
// derived state in state: two sources of truth and the effect that fails to keep them equal
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => { setTotal(items.reduce((s, i) => s + i.price, 0)) }, [items])     // one render shows items with the OLD total
// fix: const total = items.reduce(…)   (or useMemo for big lists). total cannot be wrong because it does not exist separately.

// the variant with props: copying a prop into state
function Editor({ initialText }) { const [text, setText] = useState(initialText) }   // fine: an initial value, by design
function Label({ text }) { const [t, setT] = useState(text); useEffect(() => setT(text), [text]) }   // wrong: a prop mirrored into state via an effect
// the Label renders the old prop for one frame on every change. just use the prop. if it must be editable, it is an initial value (first form), keyed to reset.

// the variant with "previous value": useEffect(() => { if (prev !== value) … }, [value]) with a ref
// often a derivation (compare in render), sometimes a real "on change" side effect (keep the effect, name it), rarely a job for getDerivedStateFromProps-style
// "adjust state while rendering": if (value !== prevValue) { setPrevValue(value); setDerived(…) } inside render: React re-renders immediately, before commit. no wrong frame.

// the variant with fetching: useEffect(() => { fetch(…).then(setData) }, [id])   → part 8: a query cache
the principle
  1. State is what cannot be computed. Everything else is a variable in render. Storing a computed value creates a second source of truth and requires something (an effect, a handler) to keep them equal; that something is where the bug lives.
  2. A prop is not an initial value unless you mean it. useState(prop) reads the prop once; later changes do not propagate. If that is intended (an editable draft seeded from the prop), key the component to the prop's identity so a new entity gets a fresh draft. If it is not intended, use the prop directly.
  3. Expensive derivations get useMemo, not state. The memo recomputes when inputs change, in the same render, with no wrong frame.
  4. "Previous value" logic is usually a derivation (compare in render), occasionally a real side effect on change (an effect with a clear name), and rarely a state adjustment during render.
the variants that look different and are the same
  1. Filtered or sorted copies in state: [visible, setVisible] kept in sync with [items] and [filter]. Derive.
  2. Form validity in state: [isValid, setIsValid] updated in an effect from values. Derive.
  3. Totals, counts, flags: all derivations.
  4. Fetched data in state after a query cache has it: two caches. Read from the cache.
  5. Server data copied into a reducer "so the store has everything": the same two caches, at scale (part 7).
56

Prop drilling and context overuse

code
// prop drilling: the honest version, the context version, and the composition version
// drilling: Page → Section → Card → Row → Button, five levels of `onSelect` that only Button uses
<Page onSelect={…}> → <Section onSelect> → <Card onSelect> → <Row onSelect> → <Button onClick={onSelect}>
// not wrong: it is explicit and type-checked. it hurts when the intermediate components are generic layout and the prop is one of twelve.

// context: for a value that is "ambient" to a subtree (the selection, the current user, the theme). the intermediates stop caring.
const Selection = createContext(null); <Selection value={{ selected, select }}>…<Button onClick={() => select(id)} />
// right when the value is dependency-like and changes rarely; wrong when it is per-keystroke state (every consumer renders: part 2)

// composition: pass the component, not the props. the Page renders the Button and gives it to Section as a child.
<Section actions={<Button onClick={onSelect} />}>…</Section>          // Section renders {actions} in its slot; it never sees onSelect
// the intermediates become layout; the data lives where it is used; nothing is drilled. this is the fix most of the time.

// the tell for which you have: if the intermediate components would be the same with or without the prop, it is drilling; compose or context.
// if the intermediates transform the prop, it is data flow; keep it explicit.
prop drilling
  1. What it is: a value passed through components that do not use it, to reach one that does. Explicit and typed, so not a bug; a smell when the intermediates are generic layout and the prop count grows.
  2. The composition fix: render the consumer where the data is and pass the element down as children or a slot. The intermediates become layout that renders what it is given. This removes most drilling without introducing any shared state.
  3. The context fix: for ambient, rarely changing values (user, theme, locale, a client). A context per concern.
  4. The store fix: for shared, frequently changing state read in many places (part 7).
  5. The tell: if removing the prop would leave the intermediate components unchanged, compose or use context; if the intermediates transform it, it is data flow and belongs as props.
context overuse
  1. Frequently changing state in context: every consumer renders per change (part 2). A form's values, a cursor, a scroll position, a live counter in context is a tree-wide render per tick.
  2. One context for everything: an AppContext with user, cart, theme and settings couples unrelated consumers; a theme change renders the cart.
  3. A value object rebuilt each render: value={{ a, b }} renders consumers even when a and b are unchanged. useMemo it.
  4. Context as a bus: a context used to call functions across the tree (an event emitter in disguise). The functions are stable; the pattern is fine; put the functions in their own context so consumers that only call do not render on state changes.
  5. The fixes: split by change rate; memoise values; separate state and dispatch contexts; move hot state to a store with selectors; keep context for dependency injection and compound components.
57

Premature memo, and the volume problems memo cannot fix

code
// premature memo: what it costs and when it pays
const Row = React.memo(function Row({ item, onSelect }) { return <li onClick={() => onSelect(item.id)}>{item.name}</li> })
// cost per render attempt: shallowEqual over props (a few compares); the hook node for the memo. benefit: skipping Row's render when props are equal.
// Row's render is a few hundred nanoseconds. the compare is a few tens. the saving is real only if props ARE equal (stable onSelect) AND the parent
// re-renders often without changing items. with an inline onSelect at the parent, the compare always fails: pure cost.

// the order of operations for a slow list: 1. is the parent rendering too much? (colocate state). 2. is the list too long for the DOM? (virtualise).
// 3. are the rows' props stable? (useCallback the handlers; useMemo the derived arrays; or enable the Compiler). 4. THEN memo the rows.
// 4 before 1 is the anti-pattern: memo everywhere, parents still re-render everything, and now every component pays a compare.

// useMemo on cheap values: useMemo(() => a + b, [a, b]) costs more than a + b. memo computations that show in a profile, and references that feed memo'd children or deps.
// useCallback on every handler: harmless but noisy; the Compiler makes it moot. without the Compiler: useCallback only for handlers passed to memo'd children or used in deps.
what memo costs and when it pays
  1. React.memo: a shallow compare per render attempt; pays when the compare passes often and the render it skips is non-trivial or has a big subtree. Fails to pay when props are unstable (inline functions, objects), when the component is cheap, or when the parent rarely renders.
  2. useMemo: a deps compare plus a slot; pays for expensive computations and for references that feed memo'd children or dependency arrays. Costs more than it saves for a + b.
  3. useCallback: only meaningful downstream; alone it is a hook node for nothing.
  4. The Compiler makes most of this moot by generating the right memoisation; the remaining judgement is whether a given component's render is worth skipping at all, which it decides by caching everything cheaply.
the order of fixes for "it is slow"
  1. Measure: the Profiler's flame chart and "why". Which components, how often, why.
  2. Reduce what renders: colocate state; split context; stop the effect chain. This removes renders.
  3. Reduce what exists: virtualise long lists; uncontrol large forms; paginate. This removes work per render.
  4. Stabilise references: useCallback/useMemo at the parent, or the Compiler. This makes bailouts possible.
  5. Memo the boundaries: React.memo on components that now receive stable props and have a subtree worth skipping.
  6. Micro-optimise the render only if a specific component's own time is the cost (the JS course part 14).
volume problems
  1. 5,000 DOM rows are slow to lay out and paint whatever React does. Virtualise (the algorithms course part 1) or paginate.
  2. A 60-field controlled form renders 60 fields per keystroke. Uncontrol it (part 9).
  3. A chart re-rendering 10,000 SVG nodes on hover. Canvas, or a hover layer separate from the data layer.
  4. A table recomputing a sort of 100k rows per render. Sort once per data change (useMemo on the data, not per row).
58

The catalogue, by symptom

Anti-patternWhat it looks likeWhy it hurts (mechanism)FixPart
Effect chainEffects setting state from state; several commits per actionEach link is a Default-lane render after paint; intermediate frames are wrongDerive in render; do action work in handlers; key to resetP11, P4
Derived state in stateA total, a filtered list, a validity flag in useState with an effect syncing itTwo sources of truth; one frame stale per changeCompute in render; useMemo if expensiveP11
Prop copied into stateuseState(prop) + an effect mirroring prop changesA stale frame per change; the effect is a chain linkUse the prop; or key the component when it is an initial valueP11
Fetch in useEffectloading/data/error state per componentWaterfalls; no cache; races; piecemeal loadingRoute loaders; a query cache; SuspenseP8
State too highPage-level state for a leaf inputWhole subtree renders per keystrokeColocate; split draft from committedP7, P10
Context as a storeHot state in a context; one context for allEvery consumer renders per change; no selectorsSplit; memoise values; a store with selectorsP7, P2
Prop drilling through layoutProps threaded through components that do not use themCoupling and noise, not performanceCompose (pass elements); context for ambient valuesP10
Inner component definitionsA component declared inside another's renderNew type each render → remount, state lossHoist to module scopeP2
Index or random keyskey={i}, key={Math.random()}Position pairing or remount every renderStable ids from dataP2
Unstable props to memoInline objects and functions into React.memo childrenShallow compare always fails; pure costuseCallback/useMemo at the parent; the Compiler; or no memoP2, P11
Premature memomemo everywhere before measuringCompares and slots for no skipped workMeasure; fix renders first; memo boundaries lastP11
Controlled everythingEvery field controlled at the form levelA form render per keystrokeUncontrolled; per-field subscriptionsP9
Layout in useEffectMeasuring and setting state in a passive effectTwo paints: a flashuseLayoutEffect for measure-and-adjustP1, P4
Refs read in renderref.current during renderNull on first render; unsafe under concurrencyRead in effects and handlersP4
Hand-rolled store subscriptionsuseEffect + useState around an external storeTearing under concurrent renderinguseSyncExternalStoreP5
Environment reads in renderwindow, Date.now, localStorage during renderHydration mismatch; tearingEffects; uSES with a server snapshotP0, P13
Suspense boundary too highOne boundary around the pageEverything hides for the slowest queryNest boundaries per regionP5
No virtualisationThousands of rows in the DOMLayout and paint cost, whatever React doesWindowingAlgorithms P1
the pointer
Part 12 is testing: what to test at which level so that fixing these does not break the suite, and the suite catches the regressions. Most of the table is visible in the Profiler; the next part is making it visible in CI.
THE ANTI-PATTERNS, BY SYMPTOM
what you see in the Profiler, and what it means
swipe the figure sideways, or tap expand for full screen
1/6
whole tree lights
Signature: one keystroke lights up the whole tree; "why did this render" says "parent rendered" everywhere. Cause: state lifted too high (the page owns the input's value) or a context whose value changes per keystroke. Fix: colocate (part 10), split the context, or a store with selectors (part 7).