All articles
·6 min read

useMemo vs useCallback vs React.memo: When to Use Each

ReactPerformanceHooksMemoization

Three React APIs get reached for the moment someone says “let's optimize this”: useMemo, useCallback, and React.memo. They sound interchangeable. They're not — and reaching for the wrong one, or all three everywhere, is how you end up with code that's harder to read and no faster. Here's the mental model I use to pick the right one in a second.

Reaction GIF about trying to tell three nearly identical things apart
Me, reading the useMemo, useCallback, and React.memo docs back to back

The short version

  • useMemo memoizes a value — the result of an expensive calculation, so it isn't recomputed on every render.
  • useCallback memoizes a function — it keeps the same function identity between renders so children don't see a “new” prop.
  • React.memo memoizes a component — it skips a child's re-render when its props are shallow-equal to last time.

Value, function, component. Once you tie each API to what it wraps, the choice stops being a guess.

useMemo — cache a computed value

useMemo runs your function and remembers its return value, recomputing only when a dependency changes. Use it when a calculation is genuinely expensive, or when the value's identity matters downstream.

const sortedRows = useMemo(() => {
  // re-sorts only when `rows` changes, not on every render
  return [...rows].sort((a, b) => b.score - a.score);
}, [rows]);

Two legitimate reasons to reach for it: the computation is heavy (sorting or filtering a large list, parsing, derived aggregates), or the result is an object/array you pass to a memoized child or a hook dependency array — where a fresh reference every render would defeat the memoization.

useCallback — keep a function's identity stable

Every render creates brand-new function objects. Usually that's harmless. It stops being harmless the moment that function is a prop to a React.memo child or a dependency of another hook — a new identity each render silently breaks the optimization.

const handleSelect = useCallback((id: string) => {
  setSelected(id);
}, []); // same function identity across renders

useCallback(fn, deps) is really just useMemo(() => fn, deps) with nicer ergonomics for functions. If nothing consumes the function's identity, wrapping it buys you nothing.

React.memo — skip a child's re-render

By default a component re-renders whenever its parent does, even if its props are identical. React.memo wraps a component and does a shallow compare of props: same props, no re-render.

const Row = React.memo(function Row({ label, onSelect }) {
  return <li onClick={onSelect}>{label}</li>;
});

The catch: a shallow compare means object, array, and function props must keep a stable identity — which is exactly why React.memo so often needs useMemo and useCallback beside it. They're a team, not competitors.

Cheat sheet

You want to…Reach forIt wraps
Avoid recomputing an expensive valueuseMemoA value
Keep a callback's identity stable for a child/hookuseCallbackA function
Stop a child re-rendering on unchanged propsReact.memoA component

When memoization makes things worse

Memoization isn't free. Every useMemo and useCallback stores a value plus its dependency array and runs a comparison on each render. For a cheap calculation, that bookkeeping can cost more than just recomputing. Wrapping everything “to be safe” adds noise, hides real bottlenecks, and can even slow renders down.

The rule I follow: don't memoize on spec — memoize when the profiler tells you to. If a value is a number from a quick expression, or a function nothing memoized consumes, leave it plain.

The mistake that quietly breaks everything

Admiral Ackbar from Star Wars shouting It's a trap
That innocent-looking inline object you just passed as a prop

The bug that's bitten me more than once isn't missing memoization — it's memoization that quietly does nothing because a dependency picks up a fresh identity on every render:

// ❌ options is a new object every render, so this never hits
const options = { sort: true };
const config = useMemo(() => build(options), [options]);

// ✅ stabilize the dependency first
const options = useMemo(() => ({ sort: true }), []);
const config = useMemo(() => build(options), [options]);

The whole game is referential identity. A child wrapped in React.memo still re-renders if you hand it a fresh object or an inline arrow function every render. That interplay — stable props feeding memoized children — is the same thing I dug into in how I cut unnecessary React re-renders by 40%.

The takeaway

Match the tool to what it wraps — value, function, component — and only reach for it when something actually told you to, which usually means the profiler did. The combo that's earned its keep in my own code: React.memo on a heavy child, fed props you kept stable with useMemo and useCallback. Scatter all three at random and you haven't optimized anything — you've just added ceremony.

FAQ

Is useCallback just useMemo for functions?

Essentially yes. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). useCallback returns the function itself; useMemo returns whatever the function returns.

Should I wrap every component in React.memo?

No. React.memo only helps when a component re-renders often with the same props and its render isn't trivial. Applied everywhere, the prop-comparison overhead can outweigh the savings.

Does the React Compiler make these obsolete?

It reduces the need for manual memoization by auto-memoizing where it's safe, but understanding value-vs-function-vs-component still matters for reading code, debugging, and every codebase not yet on it.