Re-renders and memo

Know what actually causes a re-render, and reach for memoisation only where it earns its place.

14of 20
≈ 12Min read
7Parts

What causes a re-render

That second one surprises people: a child re-renders because its parent did, even if every prop is identical. React does not compare props by default, because comparing is not free and re-rendering is usually cheap.

  • Its own state changed.
  • Its parent re-rendered — whatever the props are.
  • A context it reads changed.
Re-render is not repaint

A re-render runs your function and diffs the result. If nothing changed, no DOM is touched. The cost is the function call, not the browser’s layout — which is why most "unnecessary" re-renders cost microseconds.

React.memo

memo compares the props shallowly with Object.is. If they all match, the component is not called at all and its subtree is left alone.

skip the render when the props are the same
const Row = React.memo(function Row({ door, onPick }) {
  return <li onClick={() => onPick(door.id)}>{door.name}</li>;
});
One new object undoes it

<Row door={{ ...door }} /> or onPick={() => pick(id)} creates a new value every render, so the comparison always fails and memo is pure overhead. Memoising a component means committing to stable props.

useCallback and useMemo

the two ways to keep an identity stable
// same function object between renders
const handlePick = React.useCallback((id) => {
  setPicked(id);
}, []);                          // no dependencies: it uses only the setter

// same value, and the work is skipped
const total = React.useMemo(
  () => doors.reduce((sum, d) => sum + d.days, 0),
  [doors]
);
HookCachesUse it when
useMemoA valueThe calculation is genuinely expensive, or the value is a dependency somewhere else
useCallbackA functionThe function is passed to a memoised child or used in a dependency array
React.memoA whole renderThe component is expensive and its props really are stable

When it is worth it

Outside those, memoisation costs more than it saves: every useMemo is an array to allocate, dependencies to compare, and a line of code that can go stale.

  • A list of hundreds of rows where one row changes.
  • A component doing real work per render — a chart, a big table, a syntax highlighter, heavy formatting.
  • A value that other hooks depend on, where a new identity would re-run an effect on every render.
  • A context value object, which otherwise re-renders every consumer on every parent render.

Measure, do not guess

"This re-renders too often" is a hypothesis. The Profiler turns it into a measurement, and the measurement is usually that the slow part is somewhere else entirely — a layout thrash, an unkeyed list rebuilding, or a request.

the Profiler tells you the truth
// React DevTools → Profiler → record an interaction
// it shows which components rendered, why, and how long each took

// in code, when you need a number
console.time("render"); …; console.timeEnd("render");

Cheaper than memoising

Both of these are structural fixes with no hooks, no dependency arrays and nothing to go stale. Try them before reaching for memo.

move the state down
// every keystroke re-renders the whole page
function Page() {
  const [query, setQuery] = React.useState("");
  return (<><Search value={query} onChange={setQuery} /><HugeChart /></>);
}

// the state now lives where it is used; HugeChart never hears about it
function Page() {
  return (<><SearchPanel /><HugeChart /></>);
}
or pass children through
// Slow is created OUTSIDE, so Wrapper re-rendering does not re-render it
<Wrapper>
  <Slow />
</Wrapper>

The compiler

The React Compiler memoises components and values automatically, at build time, by analysing what each render actually depends on. Where it is enabled, most hand-written useMemo and useCallback becomes unnecessary — which is a good reason not to scatter them everywhere today.

Practice