Blog
3 min read

useMemo vs useCallback (and React.memo): When Memoization Helps

useMemo caches a computed value; useCallback caches a function; React.memo skips re-rendering a component whose props didn't change. When each actually helps, when it's wasted effort, the dependency array rules, and how the React Compiler now does most of this for you.

React re-renders a component whenever its state, props or parent change. Usually that's fast and fine. Sometimes it isn't, and React offers three memoisation tools to skip unnecessary work.

The three tools

Caches Use to
useMemo A value Avoid recomputing something expensive; keep an object/array reference stable
useCallback A function Keep a function reference stable between renders
React.memo A component's render Skip re-rendering when props are unchanged

useCallback(fn, deps) is exactly useMemo(() => fn, deps) — it's a shortcut.

useMemo: expensive calculations

const visibleOrders = useMemo(
  () => orders.filter(o => o.status === filter).sort(byDate),
  [orders, filter],
)

The filter and sort only rerun when orders or filter change, not on every keystroke elsewhere in the component.

Worth it when the calculation is genuinely costly (thousands of items, heavy transforms). Not worth it for a + b — the memoisation bookkeeping costs more than the work.

Stable references: the real reason they exist

React compares props and dependencies by reference. A new object or function is created on every render, so it's "different" every time:

function Parent() {
  const options = { sort: 'date' }          // new object every render
  const onSelect = (id) => setSelected(id)  // new function every render
  return <ExpensiveList options={options} onSelect={onSelect} />
}

Even if ExpensiveList is wrapped in React.memo, it re-renders every time, because its props are "new". Stabilise them:

const options = useMemo(() => ({ sort: 'date' }), [])
const onSelect = useCallback((id) => setSelected(id), [])

The same applies to effect dependencies: an object or function in a useEffect dependency array that's recreated every render makes the effect run every render — a classic cause of loops. (Maximum update depth exceeded, React useEffect explained)

And to context values: an unstable provider value re-renders every consumer. (React Context)

React.memo: skipping a child's render

const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
  // ...
})

React.memo only helps if the props really are stable — which is why it's usually paired with useMemo/useCallback in the parent.

The dependency array

Both hooks take dependencies, like useEffect:

  • Include every value from the component used inside.
  • Missing dependencies give you stale values (a callback that always sees the first count).
  • The react-hooks/exhaustive-deps lint rule catches this. (Linters and formatters)

For functions that only update state, the functional form avoids dependencies entirely:

const increment = useCallback(() => setCount(c => c + 1), [])

The React Compiler changes the picture

The React Compiler (stable since late 2025) analyses your components at build time and memoises automatically — values, functions and JSX — where it's safe. With it enabled, most hand-written useMemo, useCallback and React.memo become unnecessary. Next.js, Vite and Expo all support enabling it.

If your project uses the compiler, write plain code and reach for manual memoisation only when profiling shows a specific problem. If it doesn't, the guidance below applies.

When to bother (without the compiler)

  1. Measure first. React DevTools' Profiler shows which components re-render and how long they take. Don't optimise what isn't slow. (Why is my website slow?)
  2. Memoise expensive calculations and large lists.
  3. Stabilise values that go into dependency arrays, context providers, or memoised children.
  4. Otherwise, skip it — it adds noise and its own (small) cost.

Often a structural fix beats memoisation: move state down into the component that uses it, or pass children so they aren't re-created by a re-rendering parent.


EasySpawn runs your React app on its own server with Claude Code alongside — ask it to profile a slow screen and fix the re-renders, then check the result live. See how it works or join the waitlist.

Related: React useEffect Explained · React Context · Maximum Update Depth Exceeded · React Custom Hooks

Keep reading