When Does React Re-render? A Complete Mental Model
Cover: gradient generated for Devgains
Ask ten React developers when does React re-render, and you'll get ten slightly
different answers — usually some variation of "when state changes" that's true but
incomplete. That gap is where most React performance confusion lives: a component
re-renders and you can't explain why, so you reach for React.memo by reflex, the
re-render happens anyway, and you conclude React is unpredictable. It isn't. The rendering
model has exactly three triggers, and once you can name them, every "why did this render?"
question has a concrete answer. This guide is the hub for the Devgains React cluster: it
gives you the whole mental model, then points to the deep dives for each piece.
Quick answer: when does React re-render?
A React component re-renders when any one of three things happens:
- Its own state changes — a
useStatesetter oruseReducerdispatch runs with a new value. - Its parent re-renders — by default, rendering a component renders all of its children, regardless of whether their props changed.
- A context it consumes changes — every component reading a Context via
useContextre-renders when that context's value updates.
That's the entire list. Props changing is not a fourth trigger — a child re-renders because its parent rendered, not because you passed it a new prop. Getting that one distinction right dissolves most re-render mysteries.
Why it matters
Understanding re-renders isn't academic. It's the difference between a form that feels instant and one that lags on every keystroke, between a dashboard that updates smoothly and one that janks when a single widget ticks. Almost every React performance problem is either a render that does too much work or a subtree that renders far more often than it needs to — and you can't fix either until you can see why the render is firing.
The good news: re-rendering is usually cheap. React re-running your function and diffing the result is fast; the expensive part is when that function does real work or when a huge subtree re-renders needlessly. Knowing the three triggers tells you exactly where to look.
The model: render, then commit
A "render" in React is not a DOM update. It's React calling your component function to get a description of the UI (an element tree), then comparing that tree to the previous one — a process called reconciliation. Only the differences get written to the actual DOM. React splits every update into two phases:
- Render phase — React calls your components and builds the new element tree. This is pure and can be paused or discarded. Running it many times is normal and usually harmless.
- Commit phase — React applies the minimal set of DOM changes it found. This is where the browser actually paints.
So "re-render" means React ran your function again. It does not guarantee any DOM changed. A component can re-render 60 times a second and touch the DOM zero times if its output is identical each time. The React docs on render and commit lay out these phases precisely. Keep them separate in your head and the whole model gets simpler — most re-renders are cheap because they commit nothing.
The three triggers, in detail
1. State updates
When you call a state setter with a value different from the current one, React schedules a re-render of that component. Two things trip people up here.
First, React bails out if the new value is Object.is-equal to the old one — setting state
to the same primitive does nothing. Second, updates are batched: multiple setState
calls in the same event handler produce a single re-render, not one per call.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount((c) => c + 1); // both batched into ONE re-render
}
return <button onClick={handleClick}>{count}</button>;
}Both setters run, but React re-renders Counter once. Batching is why you rarely see the
"one render per setState" behavior people fear.
2. Parent re-renders (the one everyone forgets)
This is the big one. When a component re-renders, React re-renders all of its children by
default — it does not check whether their props changed first. If App re-renders, every
component it returns re-renders too, all the way down, unless something explicitly stops the
cascade.
function App() {
const [tick, setTick] = useState(0);
// Every second, App re-renders — and so does <ExpensiveTree/>,
// even though it receives no props that changed.
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<>
<p>{tick}</p>
<ExpensiveTree />
</>
);
}ExpensiveTree re-renders once a second for no reason. This is the source of most
"why is this rendering?" surprises — the trigger is a parent, not the component itself.
Two structural fixes beat reaching for memoization: move state down so only the small
piece that changes re-renders, or pass expensive subtrees as children so they're
created by a parent that doesn't re-render. React.memo is the escape hatch when neither
applies — the cost of re-renders
deep dive covers exactly when it earns its keep.
3. Context changes
Every component that reads a context with useContext re-renders when that context's value
changes — even if it only uses one field of a large value object. This is why putting a
frequently-changing value in a wide-reaching context can quietly re-render half your app.
The fix is to split contexts by change frequency, or to memoize the value you provide so it
only changes identity when it truly changes.
Step-by-step: how to trace a re-render
When a component renders more than you expect, work through the triggers in order:
- Did its own state change? Log or inspect the state setters. If a setter fired with a new value, that's your answer.
- Did its parent re-render? Walk up the tree. If a parent rendered, this component rendered because of the default cascade — not because of its props.
- Did a consumed context update? Check every
useContextcall. A new context value re-renders all consumers. - Open the React DevTools Profiler. Record an interaction and read "why did this render?" — it attributes each render to state, props, hooks, or parent. This turns guessing into reading.
Do this a few times and the model becomes second nature: you stop asking "why did React render this?" and start asking "which of the three triggers fired?"
Render triggers at a glance
| Trigger | What fires it | Stops the cascade? | Typical fix when excessive |
|---|---|---|---|
| State update | useState/useReducer with a new value | — | Move state down; don't lift it higher than needed |
| Parent render | Any ancestor re-rendering | React.memo (with stable props) | Pass subtree as children; memoize the child |
| Context change | New value in a consumed Context | Splitting contexts | Split by change frequency; memoize provider value |
| Force update | useReducer/key change (remount) | — | Rarely needed; usually a design smell |
Best practices
- Keep state as local as possible. State high in the tree re-renders everything below it. Colocate state with the component that uses it so re-renders stay small.
- Pass expensive trees as
children. A parent that re-renders doesn't re-create children it received as props — they keep their previous identity and skip re-rendering. - Memoize context values and split contexts. One context per change-frequency band keeps unrelated consumers still.
- Profile before optimizing. The Profiler tells you which component is slow and why. Fix that one, not everything.
- Let the compiler do the memoizing. The
React Compiler inserts memoization
automatically and more precisely than hand-placed hooks — on a supported version, prefer
it over sprinkling
useMemo.
Common mistakes
- Assuming "re-render" means "DOM update." Most re-renders commit nothing. Optimizing a render that never touched the DOM is wasted effort.
- Blaming props for a child's render. A child renders because its parent did, not
because a prop changed.
React.memois what makes props the deciding factor. - Using array index as a
key. Wrong keys make React re-render (and re-mount) the wrong rows during reconciliation — see keys in lists. - Cargo-culting
useMemo/useCallback. Memoizing trivial work costs more than it saves; it only helps at amemoboundary or for genuinely expensive computation. - Fixing derived state with an effect. Computing state from other state in
useEffectcauses an extra render — useEffect is not a lifecycle method explains why to compute during render instead.
Takeaways
- React re-renders for exactly three reasons: the component's own state changes, its parent re-renders, or a context it consumes changes. Props changing is not a separate trigger.
- "Re-render" means React ran your function — not that the DOM changed. Render and commit are separate phases; most renders commit nothing.
- The parent cascade is the most-forgotten trigger. Rendering a component renders all
its children by default; fix it structurally before reaching for
memo. - State updates are batched and skip equal values. Multiple setters in one handler produce one re-render.
- Profile, then fix the one slow render. The React Compiler can make most manual memoization unnecessary.
Ready to go deeper? Browse the React cluster for the piece-by-piece
guides — the cost of re-renders and when memoization helps,
Server Components,
Suspense and streaming,
controlled vs uncontrolled inputs, and
why useEffect isn't a lifecycle method.
FAQ
When does a React component re-render? When its own state changes (a useState or
useReducer update with a new value), when its parent re-renders (children re-render by
default), or when a context it reads via useContext changes value. Those three triggers
cover every re-render.
Does re-rendering mean the DOM updates? No. A re-render means React ran your component function and diffed the result. React only touches the DOM for the differences it finds, so a component can re-render many times while changing zero DOM nodes.
Why does my component re-render when its props didn't change? Because its parent
re-rendered. By default React renders all children when a parent renders, regardless of
props. To make props the deciding factor, wrap the child in
React.memo — and give it stable
props, or the comparison fails every time.
Does calling useState always trigger a re-render? Only if the new value differs from the
current one. React uses Object.is to compare; setting state to the same value bails out
without rendering. Multiple setters in the same event handler are also batched into a single
re-render.
How do I stop unnecessary re-renders? In order of preference: move state down so fewer
components depend on it, pass expensive subtrees as children, split or memoize context
values, and only then reach for React.memo/useMemo at a real boundary. Profile first to
confirm the render is actually costing you.
Conclusion
"When does React re-render?" has a short, exact answer: state changes, a parent renders, or a context updates — nothing else. Every mysterious render collapses into one of those three once you look, and the React DevTools Profiler will name the culprit when your memory won't. Hold onto two ideas and the rest follows: a re-render is React re-running your function, not a DOM write; and rendering a component renders its children by default. Get those right and you'll spend your time fixing the renders that actually cost something — and ignoring the thousands that don't. Work through the deep dives linked above and the whole React rendering model stops feeling like magic and starts feeling like a system you can reason about.
References
- React: Render and Commit — the two phases every update goes through, and why rendering isn't a DOM change.
- React: State as a Snapshot — how a render captures state and why updates schedule a new render.
- React: Queueing a Series of State Updates — batching and how multiple setters collapse into one render.
- React: Passing Data Deeply with Context — how context updates re-render every consumer.
- React: Profiler API — measuring which components render, how often, and why.

