Memoization in Practice
Three tools, three jobs
Section titled “Three tools, three jobs”Memoization means caching a result between renders so React can skip recomputing or re-rendering. There are three tools, and they do different jobs:
useMemo(fn, deps)— caches the return value of an expensive computation, recomputing only when a dependency changes.useCallback(fn, deps)— caches a function’s identity, returning the same function reference across renders until a dependency changes.React.memo(Component)— wraps a component so it skips re-rendering when its props are shallow-equal.
// useMemo: skip re-sorting a big list unless the list or order changes.const sorted = useMemo(() => items.slice().sort(compare), [items]);
// useCallback: keep a stable onSelect so a memoized child does not re-render.const onSelect = useCallback((id) => setSelected(id), []);
// React.memo: skip re-render when props are unchanged.const Row = React.memo(function Row({ item, onSelect }) { /* ... */ });Why useCallback and React.memo are a pair
Section titled “Why useCallback and React.memo are a pair”React.memo compares props with shallow equality (=== per prop). Primitives compare by value, but objects, arrays, and functions compare by reference — and a component creates brand-new ones on every render.
flowchart TB parent["Parent re-renders"] --> inline["creates NEW onClick, NEW style object"] inline --> memo["React.memo(Child) compares props"] memo --> fail["new reference !== old reference → shallow-equal fails → Child re-renders anyway"]
// ❌ A fresh function and object every render — React.memo(Child) never skips.<Child onClick={() => doThing()} style={{ color: "red" }} />
// ✅ Stable references — now React.memo(Child) can actually skip.const onClick = useCallback(() => doThing(), []);const style = useMemo(() => ({ color: "red" }), []);<Child onClick={onClick} style={style} />This is the crux: React.memo only pays off when every prop is referentially stable, which usually means memoizing the object/array/function props with useMemo/useCallback. Get one prop wrong and the memo does nothing.
The React Compiler changes the calculus
Section titled “The React Compiler changes the calculus”Manually keeping references stable is tedious and easy to get wrong. The React Compiler (React’s build-time optimizer) analyzes your components and automatically memoizes values and components where it is safe — effectively applying useMemo/useCallback/memo for you, everywhere, without you writing them.
Once the compiler is enabled on a codebase, the guidance flips:
- Do not hand-write
useMemo/useCallbackby default — the compiler handles the common cases. - Do keep your components pure and follow the Rules of Hooks — that is what lets the compiler optimize safely.
- Reserve manual memoization for the rare case the compiler cannot handle or a profiler still flags.
When manual memoization actually helps (today)
Section titled “When manual memoization actually helps (today)”Until the compiler is universal, hand-memoization is still worth it for a few specific cases:
- A genuinely expensive computation (sorting/filtering thousands of items, heavy math) →
useMemo. - A value/function passed to a memoized child or an effect dependency where a new reference would cause extra renders or effect runs →
useCallback/useMemo. - A large, frequently-bypassed subtree →
React.memo.
Outside those, memoizing usually costs more (memory + comparison + noise) than it saves. Measure.