the problem: every render mints fresh references
When a parent re-renders, every inline function () {{}} and every
object literal {{}} is a brand-new reference.
A child wrapped in React.memo() shallow-compares its props, sees new
references, and re-renders anyway — the memo did nothing. useMemo and
useCallback keep references stable across
renders so memoized children actually skip. useMemo caches a
value; useCallback caches a
function. Both recompute/recreate only when their
deps change.
| hook | signature | caches | re-runs when |
|---|---|---|---|
useMemo |
useMemo(factory, deps) |
the result of factory() |
a value in deps changes (referential inequality) |
useCallback |
useCallback(fn, deps) |
the function reference itself | a value in deps changes — sugar for useMemo(() => fn, deps) |
React.memo |
React.memo(Component) |
the last render output | any prop fails a shallow Object.is compare — needs stable prop refs to help |
1 · the memoization you write (edit me)
2 · Babel compiles JSX → element tree
<MemoDemo/> becomes React.createElement(MemoDemo).
React.useMemo(fn, [count]) returns the cached fib unless count
changed; React.useCallback(fn, []) returns the same function instance on
every render. The gold-check proves the cache survives an
unrelated re-render.
// (hit "compile & render" to see Babel's output)
3 · live React (memoization, proven)
Click count+1 and useMemo recomputes fib. Click other+1 and
fib is still cached (the value did not recompute) — while
the React.memo child updates only because its count prop
changed, never because onClick wiggled. The gold-check
drives this automatically.
live: — · gold: fib(1)→(2)→(3), other+1 keeps fib(3)=2, child shows Clicked: 1
intent → pattern
| intent | pattern | why |
|---|---|---|
| cache a costly value | useMemo(() => compute(x), [x]) |
skips recomputation on unrelated re-renders |
| stable callback prop | useCallback(handleClick, [dep]) |
keeps the fn ref identical so a React.memo child skips |
| stable object/array prop | useMemo(() => ({'{'}a, b{'}'}), [a, b]) |
object literals are new every render — wrap them to keep refs stable |
| skip a child's re-render | React.memo(Child) + stable props |
memo compares props shallowly; it needs BOTH value + ref stability to help |
| "just in case" memoization | don't — measure with Profiler first | deps comparison + cache storage cost more than the re-render you avoided |