React Hooks & Rendering
React Hooks & Rendering

Memoization: memo, useMemo & useCallback

Full · ~30 min
After this unit you can
  • 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

Simple

useMemo caches an expensive computation across renders

Flag

A 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.

Medium

memo skips a re-render — but only if props are shallow-equal

Flag

By 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.

Complex

useCallback keeps a handler stable — and when memoization buys nothing

Flag

A 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 memo child;
  • 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?

Flag

A memo() child still re-renders every time its parent updates. The most likely cause?

Flag

What's the difference between useMemo and useCallback?

Flag

When does adding useMemo / useCallback / memo buy nothing?

Flag

Your project turns on React Compiler (stable since v1.0, October 2025). What happens to the advice in this unit?

Flag

Fill in the blank

Cache this expensive sort so it only recomputes when items changes.

Flag
const sorted = (() => items.slice().sort(compare), []);

Give this handler a stable identity so a memo() child doesn't re-render.

Flag
const handleClick = (() => onSelect(id), [onSelect, ]);

Wrap this child so it skips re-rendering when its props are shallow-equal.

Flag
const 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")} />
    </>
  );
}
Flag
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} />
    </>
  );
}
Flag

Challenge

Stop the memo child from re-rendering

Flag

Child 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

Loading the in-browser editor…

Further reading