codejump

AcademyReact

What useMemo and useCallback Memoize

useMemo caches a value, useCallback caches a function reference — the same mechanism underneath, aimed at two different bugs.

4 min readUpdated

3 exercises

Waiting at the end of this page.

Two hooks, one mechanism

Both hooks store a value from a previous render and hand it back unchanged on the next one, as long as the dependency array compares equal (per-entry, with Object.is) to last time. Where they differ is entirely in what gets stored:

JavaScript
const total = useMemo(() => computeTotal(items), [items]);
//    ^ the RESULT of calling the function is cached

const handleClick = useCallback(() => submit(items), [items]);
//    ^ the FUNCTION ITSELF is cached, uncalled

That is not an analogy — it is a literal implementation detail worth memorizing because it removes the need to keep the two straight from memory:

JavaScript
useCallback(fn, deps) === useMemo(() => fn, deps)

useCallback(fn, deps) is useMemo where the thing being memoized happens to be a function value rather than a computed one. Every question about "does useCallback also memoize the return value of fn" dissolves once you see this — fn is never called by either hook. It is just handed back, the identical reference, until a dependency changes.

Two different problems, not one

useMemo exists to skip an expensive recomputation. If computeTotal walks a 10,000-item array and items has not changed, there is no reason to walk it again just because the component re-rendered for an unrelated reason (a sibling's state changed, a parent re-rendered).

JSX
function Cart({ items, currency }) {
  const total = useMemo(() => computeTotal(items), [items]);
  return <p>{formatMoney(total, currency)}</p>;
}

useCallback exists for a completely different reason: identity, not computation cost. A function created inline is a new reference every render — () => {} today is not === to () => {} from the last render, even though they do the same thing. That matters exactly when something downstream compares references to decide whether to bail out of work:

JSX
const handleSelect = useCallback((id) => setSelectedId(id), []);
return <ExpensiveList onSelect={handleSelect} items={items} />;

If ExpensiveList is wrapped in React.memo, a new onSelect reference every render defeats that memoization — React.memo's shallow prop comparison sees a changed prop and re-renders the child anyway, regardless of how expensive that child is. useCallback is what keeps onSelect stable across renders so React.memo actually has something stable to compare against.

The mistake that makes useMemo cost more than it saves

Memoization is not free. Every render, React still has to compare the new dependency array against the old one, entry by entry, and hold onto the cached value between renders. For a computation that costs less than that bookkeeping — string concatenation, a small array filter, basic arithmetic — wrapping it in useMemo is pure overhead with no payoff:

JSX
// Not worth it — the comparison costs more than just recomputing
const fullName = useMemo(() => `${first} ${last}`, [first, last]);

// Just compute it
const fullName = `${first} ${last}`;

The rule that actually holds up: reach for useMemo when you have measured a computation that is expensive relative to a render (real work over real data — sorting, filtering, or transforming a large collection; not string interpolation), and reach for useCallback when the function's identity crosses a boundary that cares about it — a memoized child's props, or another hook's dependency array. Wrapping every function and every derived value "to be safe" is not the safe default; it is a small tax paid on every single render, for a benefit that mostly does not exist.

Neither one stops the component's own re-render

This is the part that trips people who reach for useCallback expecting it to speed up the component it is declared in. It does not — the component wrapping useCallback still re-renders exactly as often as it would without it. What useCallback changes is only the identity of the value it hands to something else.

JSX
function Parent() {
  const [count, setCount] = useState(0);
  // handleClick's IDENTITY is stable across re-renders of Parent
  const handleClick = useCallback(() => console.log('clicked'), []);

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      {/* Only skips re-rendering if Child is wrapped in React.memo */}
      <Child onClick={handleClick} />
    </>
  );
}

Parent re-renders on every click, every time, useCallback or not — that is unavoidable, it owns the state that changed. useCallback only pays off downstream, and only if Child is actually memoized. Without React.memo on the receiving end, a stable onClick reference changes nothing observable at all; Child re-renders whether the function reference moved or not, because nothing is checking.

Now practice it

2 challenges · 1 quiz

Reading this page is the cheap half. These are the exercises that make you use it.