Anatomy of a Re-Render
A re-render is React doing its job — comparing what the UI should look like now against what it looked like before, then updating the DOM if anything changed. The problem isn't that re-renders happen. The problem is when they happen unnecessarily.
To optimise, you need to know exactly what triggers them.
The Three Triggers
Without any memoization, this is a pretty comprehensive list. In the baby's first React app, all the hooks and state live in the root App component — so by default, the whole tree re-renders on every state change.
1. State Changes
When a component's own state changes, it re-renders. Always. This is expected.
function Counter() {
const [count, setCount] = useState(0);
// re-renders every time setCount is called
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}2. Parent Re-renders
When a parent re-renders, all of its children re-render by default — regardless of whether their props changed.
function Parent() {
const [tick, setTick] = useState(0);
return (
<>
<button onClick={() => setTick(t => t + 1)}>Tick</button>
<ExpensiveChild /> {/* re-renders on every tick, even with no props */}
</>
);
}This is the most common source of performance problems. The child didn't change, but React re-renders it anyway because the parent did. React keeps checking children all the way down unless you explicitly stop it.
3. Context Changes
When a context value changes, every component that consumes that context re-renders — and then theoretically all of its children.
const ThemeContext = createContext({ mode: 'dark', accent: 'blue' });
function App() {
const [theme, setTheme] = useState({ mode: 'dark', accent: 'blue' });
// Changing `accent` causes ALL consumers to re-render,
// even components that only care about `mode`.
return (
<ThemeContext.Provider value={theme}>
<ModeDisplay /> {/* re-renders even if only accent changed */}
<AccentPicker />
</ThemeContext.Provider>
);
}Context has no granularity. The consumer re-renders when any part of the value changes. The common mistake is one giant context with everything in it — that's a trigger for nearly everything re-rendering all the time.
The fix: split into multiple contexts. A ThemeContext for display preferences, a separate UserContext for user data. Each context only triggers its own consumers when its specific value changes.
What Does NOT Cause a Re-Render
Passing the same props doesn't skip a re-render by itself. If the parent re-renders, the child re-renders — even if every prop value is identical. Prop equality only matters when you opt in to memoization with React.memo.
Not changing state doesn't cause a re-render. If you call setState with the exact same value (by reference for objects, by value for primitives), React bails out and skips the re-render.
const [count, setCount] = useState(0);
setCount(0); // already 0 — React skips the re-renderHow Memoization Stops the Wave
Without memoization, React calls every function and checks its output. With React.memo, React checks the inputs (props) instead:
If a component is a pure function — same inputs produce same outputs — and the props haven't changed, we don't need to call it to know the output is the same. Skip the render entirely.
const ExpensiveChild = React.memo(function ExpensiveChild({ value }) {
// only re-renders if `value` actually changed
return <div>{value}</div>;
});This stops the re-render wave from propagating down through that component. But there's a trade-off: React now has to compare props on every render above it. If the component is cheap to render anyway, the comparison might cost more than the render itself.
The Propagation Mental Model
Think of re-renders as a wave that propagates down the tree from wherever state changed.
App (state changes)
├── Header ← re-renders (child of App)
│ └── Logo ← re-renders (child of Header)
└── Main ← re-renders (child of App)
├── Sidebar ← re-renders
└── Content ← re-renders
└── List ← re-rendersEvery node in the subtree below the state change re-renders. The further down the tree your state lives, the smaller the wave.
This is why state colocation is the first optimisation to reach for. The wave can't travel up — only down.
The Children Prop Exception
There's a subtle but powerful escape hatch. If a component receives children as a prop, and those children were created outside of that component, they are not re-rendered when the component re-renders.
// ParentWithState re-renders when its own state changes.
// But `children` was created by Grandparent and passed in —
// React sees it as the same element reference and skips re-rendering it.
function ParentWithState({ children }) {
const [tick, setTick] = useState(0);
return (
<div>
<button onClick={() => setTick(t => t + 1)}>Tick</button>
{children} {/* NOT re-rendered by ParentWithState's state change */}
</div>
);
}
function Grandparent() {
return (
<ParentWithState>
<ExpensiveChild /> {/* created here, not inside ParentWithState */}
</ParentWithState>
);
}This pattern — "lifting content up" — is one of the most underused performance techniques in React. No memo, no useCallback needed.
Two Types of Re-Renders
There are really only two categories:
Necessary re-renders — the state the component cares about actually changed, and the UI needs to reflect it. If the number of to-dos on your list changed, you should re-render. That's the whole point.
Unnecessary re-renders — the component re-rendered but produced the same output. You did work that didn't matter. This is what we're trying to eliminate.
And within necessary re-renders, there's a further split: urgent vs non-urgent. Some updates must feel instant (typing into an input). Others can wait a moment (re-sorting a large list behind that input). React 18's startTransition and useDeferredValue let you declare that distinction explicitly. That's exactly what Lanes digs into.
When to Not Optimise
Re-renders are only a problem if they're expensive or unnecessary. If the entire component tree re-renders in a millisecond — so what? Don't add seven layers of memoization for gains that don't matter. Every React.memo, every useCallback, every useMemo adds complexity and a maintenance cost.
Measure first. React DevTools Profiler will show you exactly which components are re-rendering and how long they take. Only optimise what the data says is slow.
Practice what you just read.