Back to React
React
medium
mid

How does React manage re renders using React.memo, useMemo, and useCallback?

React re-renders on state change, parent re-render, context change, or external store update. Tools to prevent unnecessary renders: `React.memo(Component)` skips re-render when props are referentially equal; `useMemo(fn, deps)` caches a computed value across renders; `useCallback(fn, deps)` caches a function reference so memoized children don't bail. Combined: memoize the child, pass useCallback handlers, useMemo objects. Profile first — most uses are unnecessary.

7 min read·~5 min to think through

Three tools, one purpose: skip work that doesn't need to happen.

React.memo — skip a child re-render

tsx
const Row = memo(function Row({ item }: { item: Item }) {
  return <div>{item.name}</div>;
});

When the parent re-renders, React checks if Row's props are referentially equal to last time. If so, skips the render entirely.

Default equality is shallow (Object.is per prop). Custom compare:

tsx
memo(Row, (prev, next) => prev.item.id === next.item.id);

useMemo — cache a computed value

tsx
const sorted = useMemo(
  () => items.slice().sort(expensiveCompare),
  [items],
);

The function re-runs only when items changes. Two purposes:

  1. Skip expensive computation.
  2. Keep referential equality for downstream memoization.

useCallback — cache a function identity

tsx
const onClick = useCallback(() => doThing(id), [id]);

Sugar for useMemo(() => fn, deps). The function reference is stable as long as deps don't change. Useful when:

  • The function is passed to a React.memo'd child.
  • The function is used in another hook's deps.

Combined usage

tsx
function List({ items }: { items: Item[] }) {
  const onSelect = useCallback((id: string) => {
    console.log(id);
  }, []);

  return (
    <ul>
      {items.map(item => (
        <Row key={item.id} item={item} onSelect={onSelect} />
      ))}
    </ul>
  );
}

const Row = memo(function Row({ item, onSelect }: Props) {
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});

When the parent re-renders for some unrelated reason:

  • onSelect is stable (useCallback).
  • item is the same reference if the array hasn't changed.
  • memo skips Row's render.

Why all three are usually needed together

memo on the child is useless if the parent passes a new function each render:

tsx
<Row item={item} onSelect={() => doStuff(item.id)} />

New function each render → props look 'changed' → memo bails. useCallback fixes it.

Same with inline objects:

tsx
<Row item={item} style={{ color: 'red' }} />  // new object each render

When NOT to use

  • The component is cheap (< 1ms render).
  • Props are objects/arrays you don't memoize → memo is useless anyway.
  • The memoization overhead exceeds the saved work.

The React docs explicitly say: 'don't memoize everything; measure first'.

Compiler

React 19's compiler (formerly 'forget') memoizes automatically — useMemo/useCallback become opt-out rather than opt-in. Until then, manual.

Profile to verify

React DevTools → Profiler:

  1. Record an interaction.
  2. Check render counts per component.
  3. If memo'd components still render, inspect their props for unstable references.

Cost model

  • useMemo / useCallback / memo all add a small bookkeeping cost (dep array compare + closure allocation).
  • For cheap renders, the bookkeeping costs more than the saved work.
  • For expensive renders (large trees, heavy computation), the savings dominate.

Senior framing

The triplet only pays when the child's render is genuinely expensive OR when memoized referential identity propagates downstream. Without profiling, most uses are speculative — they protect against re-renders that wouldn't have cost anything.

Follow-up questions

  • Why doesn't React.memo work when an inline function is passed?
  • When does the React Compiler eliminate the need for these?
  • What's a custom equality function in memo good for?

Common mistakes

  • Wrapping every component in memo — most don't benefit.
  • Memoizing a value but recreating the dependency every render.
  • useCallback without a memoized consumer — pure overhead.

Performance considerations

  • Each hook adds ~100-200 ns of overhead. The saved render must exceed that. For components rendering in 0.1ms, you're often slower with memoization. Profile, don't guess.

Edge cases

  • Custom equality function in memo can be slower than re-rendering.
  • Refs as props bypass memo's prop check.
  • Context bypasses memo entirely — consumers re-render on context change.

Real-world examples

  • Typical wins: rows in long virtualized lists, expensive chart computations, complex form trees. Typical losses: tiny UI primitives, wrapper components, anything with frequent prop changes.

Senior engineer discussion

Senior framing: memoization isn't 'optimization' by default. It's a tradeoff: more bookkeeping, less re-rendering. The right uses are obvious (heavy list rows, expensive computations); the speculative uses are the bug source (stale memos, missing deps, weird re-renders).

Related questions