Skip to content

Memoization in Practice

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 }) { /* ... */ });

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 reference defeats React.memo
// ❌ 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.

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/useCallback by 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 subtreeReact.memo.

Outside those, memoizing usually costs more (memory + comparison + noise) than it saves. Measure.

What does `useCallback` cache?
Why does passing an inline `style={{ color: "red" }}` to a `React.memo` child defeat the memo?
Once the React Compiler is enabled, what is the recommended default?
Which is a genuinely good use of `useMemo` today?