DevgainsDevgainsDevgains
All articles

When Does React Re-render? A Complete Mental Model

·10 min read·Updated Jul 1, 2026
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:

  1. Its own state changes — a useState setter or useReducer dispatch runs with a new value.
  2. Its parent re-renders — by default, rendering a component renders all of its children, regardless of whether their props changed.
  3. A context it consumes changes — every component reading a Context via useContext re-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:

  1. Did its own state change? Log or inspect the state setters. If a setter fired with a new value, that's your answer.
  2. 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.
  3. Did a consumed context update? Check every useContext call. A new context value re-renders all consumers.
  4. 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

TriggerWhat fires itStops the cascade?Typical fix when excessive
State updateuseState/useReducer with a new valueMove state down; don't lift it higher than needed
Parent renderAny ancestor re-renderingReact.memo (with stable props)Pass subtree as children; memoize the child
Context changeNew value in a consumed ContextSplitting contextsSplit by change frequency; memoize provider value
Force updateuseReducer/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.memo is 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 a memo boundary or for genuinely expensive computation.
  • Fixing derived state with an effect. Computing state from other state in useEffect causes 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

10 min read

Read next