Lesson 1 of 8
What Triggers a Re-render
"Everything should be made as simple as possible, but no simpler." — attributed to Albert Einstein
Ask ten React developers what causes a component to re-render and most will say, without hesitation, "when its props change." It is the single most repeated half-truth in the ecosystem — repeated so often it feels like bedrock. It is also wrong, and the gap between what people believe and what React actually does is exactly where slow apps, mysterious bugs, and failed interviews are born. Every performance technique in this Part — memo, useMemo, useCallback, context splitting, useTransition — is a response to how rendering really works. So before we optimise anything, we have to get the trigger dead right. Get this chapter wrong and every optimisation after it is cargo-cult; get it right and most of them become obvious.
You changed one piece of state in one component and half your screen flickered with work. What exactly told React to run all those component functions again — and was it really the props, like everyone says?
Core Concepts
Back in Part I you learned React's three-beat rhythm: trigger → render → commit. A trigger schedules a render; the render phase calls your component functions to produce a new element tree; the commit phase applies only the real differences to the DOM. This chapter zooms all the way in on the first beat. There are exactly three things that trigger a re-render, and no others:
- The component's own state changes — a
useStatesetter oruseReducerdispatch runs with a new value. - The component's parent re-renders — when a component renders, React re-renders all of its children by default, regardless of whether their props changed.
- A context the component consumes changes — a
useContextvalue updates because some provider above it got a newvalue.
That is the whole list. Notice what is not on it: "props changed." This is the myth, and it is worth being precise about why it is a myth. When a parent re-renders, it re-runs its function body, which produces new child elements, and React re-renders those children — whether their props changed or not. Props changing is a consequence of the parent rendering, not the cause of the child rendering. The child would have re-rendered even if you passed it the exact same props. Props are the payload; the parent's render is the trigger.
Think of a company-wide email thread. When the CEO (a parent) sends an update, everyone below re-reads the thread — not because their own name was mentioned, but because the message went down the org chart. A new detail that concerns you (a changed prop) might be in there, or it might not; you re-read regardless, simply because someone above you posted. The trigger was "someone above sent," not "something about me changed." memo, later, is the assistant who reads it first and only forwards it to you if something you care about actually changed.
"Re-render" does not mean "touch the DOM"
Here is the distinction that separates people who understand React from people who merely use it. To re-render means React calls your component function again to get a fresh element tree. It does not, by itself, change a single pixel. The render phase is pure computation — it produces a description of what the UI should look like (React elements, plain JavaScript objects). Only afterward, in the commit phase, does React compare that description to the previous one and write to the DOM — and only where there is an actual difference.
So a component can re-render a hundred times and never cause a DOM mutation, as long as the output is the same each time. "Wasteful re-render" means wasteful function calls and diffing — not necessarily wasteful DOM writes. Keep these two nouns apart for the rest of your career: render = React runs your function; commit = React updates the DOM. They are not synonyms.
Watching it happen
Let's make the invisible visible. A console.log at the top of a component runs every time that component renders. Click the button and watch the console:
Each click calls setCount — trigger #1, own state changed — so App runs again and the log fires. Simple. Now the part that surprises people.
Cascading renders: a parent renders, so its children render
Watch what happens to a child that receives no props at all and has no state of its own. It still re-renders every time its parent does:
Every click logs both App rendered and Child rendered. Child has nothing that changed — no state, no props — yet it re-runs, because trigger #2 fired: its parent rendered. This is the default behaviour that catches everyone off guard, and it is the entire reason the optimisation chapters exist. Rendering flows down: from the component whose state changed, through every descendant, to the leaves. It does not flow up or sideways.
How experienced React developers actually think about this, day to day:
• A render starts at one component and flows down. Find the component whose state changed — that's the top of the subtree that will re-run. Everything below it re-renders by default.
• "Why did this render?" has only three answers. Own state, parent rendered, or consumed context changed. When debugging a rogue render, walk the list — it is never a fourth thing.
• Renders are cheap; commits are the expensive part — usually. A re-render that produces identical output costs a function call and a diff, not a DOM write. Panic about the right layer.
• Lift state as low as it will go. State placed high re-renders a big subtree on every change; state placed near where it's used keeps the cascade small. Placement is performance.
The bail-out: same value, no render
State updates are not unconditional. If you call a setter with a value that is Object.is-equal to the current value, React bails out — it will not re-render that component (though it may still re-render once to check). Watch: clicking here logs nothing after the first render, because the value never actually changes.
The comparison is Object.is, the same shallow check === uses for primitives. This is precisely why passing a new object or array — setUser({ ...user }) — always re-renders even when the contents look identical: two different object references are never Object.is-equal. The bail-out saves you only when the reference (or primitive) is genuinely unchanged.
On the Academy's lesson page, the top-level layout holds state for the video player's current timestamp — it ticks several times a second. Early on, the sidebar (course outline, dozens of lessons) sat as a child of that same component, so it re-rendered on every tick: not because its props changed, but because its parent did. The outline output was identical each time, so the DOM never moved — but React was calling and diffing the whole sidebar tree many times a second for nothing. The fix wasn't memo first; it was recognising the trigger (parent renders → children render) and moving the fast-ticking timestamp state down into the player, so the sidebar was no longer a descendant of the thing that changed. Understanding the trigger made the fix structural, not a patch.
When does any of this actually matter?
Honestly: often it doesn't. React is fast. Re-rendering a few dozen simple components produces identical output, React diffs it, finds nothing to commit, and moves on in well under a frame. Chasing every re-render out of an app that feels instant is textbook premature optimisation — you add memo and useCallback everywhere, make the code harder to read, and gain nothing measurable.
It starts to matter when the numbers get real: large trees (hundreds or thousands of nodes re-running on every keystroke), expensive children (a component doing heavy computation or rendering a big list in its body), or high-frequency triggers (animations, drag, fast-typing, timers). There, unnecessary renders turn into dropped frames a user can feel. The skill isn't "never re-render" — it's knowing the trigger well enough to tell a harmless cascade from a costly one, and to reach for the right tool. The rest of this Part is those tools. This chapter is the diagnosis they treat.
Common Mistakes
- "A component re-renders when its props change." The most common one, and false. It re-renders because its parent rendered; the props may or may not have changed. Passing identical props to an ordinary (non-
memo) child does not stop it re-rendering. - Equating "render" with "DOM update." They are different phases. A render is React calling your function; the DOM only changes in commit, and only for real differences. Most re-renders touch nothing.
- Thinking renders flow upward. Changing a child's state never re-renders its parent. Rendering starts at the component whose state changed and flows down. If a parent needs to react, the state belongs in the parent (lifted up), not the child.
- Expecting
setStatewith the same value to always skip work. It bails out on anObject.ismatch — great for primitives, useless for a fresh object/array literal, which is a new reference every time and always re-renders. - Blaming re-renders for a slow app before measuring. Renders are usually cheap. Profile first (React DevTools Profiler); optimise the tree that's actually expensive, not every component you can see.
- Reaching for
memobefore fixing structure. Often the real fix is moving state down so the expensive subtree isn't a descendant of the fast-changing component in the first place.
You have a page: <App> holds a search string in state and renders a <SearchBox> and a <Results> list of 5,000 rows. The user types one character. Which components run their function again — and does the row list change in the DOM? Answer: App re-renders (its state changed), and because it re-rendered, both children re-render too — including all 5,000 rows, even though the results list is identical until the query is submitted. React will diff those 5,000 rows and, finding no differences, commit nothing to the DOM. So: 5,001 function calls, one giant diff, zero DOM writes — on every keystroke. Now you can feel exactly which knob to turn (move the input's state out of App, or memo the list), and why the naive version stutters.
Interview Corner
useState setter or useReducer dispatch, its parent re-renders, or a context it consumes changes. People often say 'when props change,' but that's really a symptom of the parent re-rendering — a child re-renders because its parent did, whether or not the props actually changed."memo can skip that based on props.memo them and make sure their props are referentially stable (useCallback/useMemo), and consider useDeferredValue to keep typing responsive while the expensive list catches up. The mental order is always: find where the render starts, understand what it cascades into, then choose structure over memoisation before memoisation over nothing."memo. Junior engineers sprinkle memo; senior engineers move state so there's less to memoise.Object.is bail-out, and describe how a render cascades down the fiber tree. They probe until you hit the mechanism, not just the API.- In the cascading-render editor above, add a second child
<Sibling>with its ownconsole.log. Predict how many logs one click produces before you run it, then confirm — and explain which trigger fired for each component. - Give
Childits ownuseStateand a button that updates it. Confirm that clicking the child's button re-rendersChildbut notApp— proving renders flow down, never up. - Replace
setN(5)in the bail-out example withsetN(prev => prev)and then withsetN({}). Explain, usingObject.is, why one still bails out and the other re-renders every click. - Out loud (as in a micro1 screen), state the three triggers and then explain in one breath why "a component re-renders when its props change" is misleading.
Chapter Summary
- Exactly three things trigger a re-render: the component's own state changes, its parent re-renders, or a context it consumes changes. There is no fourth.
- "A component re-renders when its props change" is a myth. It re-renders because its parent rendered; changed props are a consequence, not the trigger. Ordinary children re-render even with identical props.
- Render ≠ DOM update. A render is React calling your function to produce a new element tree; the DOM only changes in the commit phase, and only where there's a real difference.
- Rendering flows down: it starts at the component whose state changed and re-runs every descendant by default. It never flows up or sideways.
- A
setStatewith anObject.is-equal value bails out — which is why primitives can skip work but fresh object/array literals always re-render. - Most re-renders are cheap because they commit nothing; they matter for large trees, expensive children, and high-frequency triggers — measure before optimising, and prefer moving state down to sprinkling
memo.
We now know precisely when React re-runs a component and that a render only produces a new element tree — a description, not a DOM change. Which raises the obvious next question: given the old tree and the new one, how does React decide what actually changed and what to leave alone? That comparison is the beating heart of React's performance, and it has rules — some elegant, some pragmatic, one or two genuinely surprising. Next: Reconciliation & the Diffing Algorithm.
Editorial reflection — the myth that "props cause re-renders" survives because it's usually close enough to be useful and rarely punished. But "close enough" is exactly what fails you under load and under interview pressure — the two moments that decide careers. The engineers who write consistently fast React aren't the ones who memorised the most optimisation hooks; they're the ones who can say, without hesitating, why any given component just re-rendered. Master the trigger, and every tool in this Part stops being a spell you cast and becomes a decision you make.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.