Memoization: memo, useMemo & useCallback
- Explain how memo() skips a re-render only when every prop is shallow-equal by Object.is
- Predict whether a memo child re-renders when its parent passes inline objects or functions
- Choose between useMemo (cache a value) and useCallback (cache a function), with the right deps
- Fix a memo child's needless re-renders by giving its callback prop a stable identity with useCallback
- Spot memoization that buys nothing — cheap work that no memo child or Hook dependency consumes — and say what React Compiler changes
Read
useMemo caches an expensive computation across renders
FlagA component re-renders often. If it recomputes something expensive every time — sorting a big list, filtering, a heavy calculation — you pay that cost on every render, even when the inputs didn't change.
useMemo(factory, deps) runs the factory on the first render and caches the result.
On later renders it hands back the cached value, and runs the factory again only when
a value in deps has changed (compared with Object.is).
function Report({ rows, query }) {
// ❌ re-sorts on every render, even when `rows` didn't change:
// const sorted = rows.slice().sort(bySize);
// ✅ re-sorts only when `rows` changes:
const sorted = useMemo(() => rows.slice().sort(bySize), [rows]);
return <Table rows={sorted} />;
}
Rule of thumb: reach for useMemo when the computation is genuinely expensive —
or when you need the result to keep a stable reference across renders.
memo skips a re-render — but only if props are shallow-equal
FlagBy default, when a parent re-renders, all its children re-render too. React.memo
wraps a component so it skips re-rendering when its props are shallow-equal to
last time (Object.is on each prop).
const Row = memo(function Row({ label }) {
return <li>{label}</li>;
});
The catch is referential identity. memo compares references, not contents.
An inline object, array, or function is created fresh on every parent render, so
it's a new reference every time — and memo's shallow check fails:
function List({ items }) {
return items.map((it) => (
// ❌ new arrow + new object each render → Row re-renders every time
<Row key={it.id} label={it.label} onClick={() => pick(it)} style={{ padding: 4 }} />
));
}
memo only pays off when the props you pass are stable. For primitive props
(strings, numbers, booleans) that's automatic. For objects and functions, you have
to stabilize the reference yourself — that's what useMemo and useCallback are for.
useCallback keeps a handler stable — and when memoization buys nothing
FlagA memo child that takes a function prop re-renders on every parent update if the
function is defined inline, because the arrow gets a new identity each render.
useCallback(fn, deps) returns the same function reference until a dep changes,
so the child's shallow prop check passes and it skips the re-render.
const Child = memo(function Child({ onSave }) {
console.log("Child render");
return <button onClick={onSave}>Save</button>;
});
function Form() {
const [draft, setDraft] = useState("");
const [count, setCount] = useState(0);
// ✅ stable across renders; recreated only if `draft` changes
const onSave = useCallback(() => api.save(draft), [draft]);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<Child onSave={onSave} /> {/* skips re-render when only `count` changes */}
</>
);
}
Bumping count re-renders Form, but onSave keeps its identity, so memo skips
Child. Note the deps: because the closure reads draft, it must be a dep —
otherwise onSave would save a stale draft.
react.dev's simplified model: useCallback(fn, deps) behaves like
useMemo(() => fn, deps) — one caches a function, the other caches a value.
When memoization buys nothing
react.dev names the cases where it pays off:
- a calculation that is noticeably slow (measure it: around 1ms or more) and whose inputs rarely change;
- a value or function passed to a
memochild; - a value or function used as another Hook's dependency (an Effect's, say).
Anywhere else it skips nothing. The comparison itself is cheap — react.dev calls
over-memoizing "no significant harm" — but it isn't free in other ways: the code is harder
to read, every dep array is one more thing to keep correct (a missing dep is a stale-value
bug), and a single always-new prop silently turns the whole chain into dead weight. Wrap a
child in memo and stabilize its object and function props, or do neither.
Quiz
What does wrapping a component in React.memo() actually do?
FlagA memo() child still re-renders every time its parent updates. The most likely cause?
FlagWhat's the difference between useMemo and useCallback?
FlagWhen does adding useMemo / useCallback / memo buy nothing?
Your project turns on React Compiler (stable since v1.0, October 2025). What happens to the advice in this unit?
FlagFill in the blank
Cache this expensive sort so it only recomputes when items changes.
const sorted = (() => items.slice().sort(compare), []);
Give this handler a stable identity so a memo() child doesn't re-render.
Flagconst handleClick = (() => onSelect(id), [onSelect, ]);
Wrap this child so it skips re-rendering when its props are shallow-equal.
Flagconst Row = (function Row({ label }) { return <li>{label}</li>; });
Predict the output
const Child = memo(function Child({ onClick }: { onClick: () => void }) {
console.log("Child render");
return <button onClick={onClick}>child</button>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<Child onClick={() => console.log("clicked")} />
</>
);
}const Child = memo(function Child({ style }: { style: object }) {
console.log("Child render");
return <div style={style}>hi</div>;
});
function Parent() {
const [n, setN] = useState(0);
const style = useMemo(() => ({ color: "red" }), []);
return (
<>
<button onClick={() => setN(n + 1)}>{n}</button>
<Child style={style} />
</>
);
}Challenge
Stop the memo child from re-rendering
FlagChild is wrapped in React.memo, so it should skip re-rendering while its props
stay shallow-equal. But every time you click Tick in Parent, the child's
render count climbs anyway.
The reason: Parent defines handleChildClick fresh on every render, so it has a
new identity each time. memo's shallow prop check sees a "different" onClick
and re-renders the child on every unrelated update — the memo buys nothing.
Your task
Edit Parent.tsx so the tests pass:
- The child renders once on mount (render count shows 1)
- Clicking Tick does not re-render the child (render count stays 1)
- The child's own Child action button still fires its callback, and that doesn't re-render the child either