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.
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.
const Row = React.memo(function Row({ door, onPick }) {
return <li onClick={() => onPick(door.id)}>{door.name}</li>;
});
<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
// 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]
);
| Hook | Caches | Use it when |
|---|---|---|
useMemo | A value | The calculation is genuinely expensive, or the value is a dependency somewhere else |
useCallback | A function | The function is passed to a memoised child or used in a dependency array |
React.memo | A whole render | The 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.
// 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.
// 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 /></>);
}
// 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.