parent re-render → child re-render (unless memoized)
When a parent re-renders, React re-renders every child by
default — even children whose props did not change. React.memo(Component)
wraps a component so React first runs a shallow prop comparison
(Object.is per prop); if every prop is referentially equal, React
skips the re-render and reuses the last rendered output.
memo is a performance optimization, never a correctness
mechanism — the un-memoized app renders identically, just slower.
| piece | role | analogy |
|---|---|---|
React.memo(C) |
higher-order component returning a memoized version of C |
a bouncer at the component door — checks the props "ID" before letting a re-render in |
| shallow compare | Object.is(prevProps[key], nextProps[key]) for every key |
compares the envelope, never opens it — only references, not deep contents |
| 2nd arg comparator | (prevProps, nextProps) => areEqual — return true to SKIP |
a custom bouncer who opens only the props you name |
| when it breaks | parent passes a fresh function/object/array literal each render | new envelope every time → bouncer always lets the re-render through |
1 · the memo you write (edit me)
2 · Babel compiles JSX → element tree
<MemoChild/> becomes React.createElement(MemoChild).
On parent re-render React calls Object.is on each prop; if all match,
the memoized child's body never executes and renderCount.child stays put.
// (hit "compile & render" to see Babel's output)
3 · live React (the skip, proven)
Click other+1 — the parent re-renders but the child's
title prop is unchanged, so React.memo skips it (render count
holds). Click count+1 — title changes, the
shallow compare fails, and the child re-renders. The gold-check
drives this automatically and asserts the render count at every step.
child renders: — · gold: note init → other+1 (hold) → count+1 (rise) → other+1 (hold)
intent → pattern
| intent | pattern | why |
|---|---|---|
| skip a child when props are unchanged | const C = React.memo(function C(props){...}) |
shallow Object.is per prop → reuse last output |
| keep a function prop stable | useCallback(fn, deps) in the parent |
without it the handler is a fresh ref each render → memo always re-renders |
| keep an object/array prop stable | useMemo(() => ({...}), deps) in the parent |
object/array literals are new refs every render → memo always re-renders |
| compare only some props | React.memo(C, (prev, next) => prev.id === next.id) |
return true to SKIP, false to re-render |
| decide IF to memo | profile first; wrap only measurably expensive trees | memo has its own compare cost — wrapping cheap components is a net loss |