Rendering and reconciliation
What triggers a render, what render and commit each do, and the rule that decides whether a component keeps its state - same type in the same position.
After this lesson you can
- List what causes a component to re-render, and what does not
- Separate the render phase from the commit phase
- Predict whether a component keeps or loses its state when the tree changes shape
- Explain why defining a component inside another component resets its state
"Rendering" in React means one specific thing: React calls your component function to find out what it should look like now. It does not mean the DOM was touched.
Render, then commit
An update goes through two phases:
- Render. React calls the components that need updating. Each returns a new tree of elements. React compares that tree with the one from the previous render — this comparison is reconciliation — and works out which DOM changes are needed.
- Commit. React applies those changes to the DOM, attaches refs, and then runs effects.
A render that produces the same output as last time leads to a commit with nothing to change. Renders are meant to be cheap and pure; the DOM work is what React keeps small.
What triggers a render
- The first mount.
- A state update in the component (
setState,dispatch). - A change in a context value the component reads.
- Its parent rendered. By default, when a component renders, every component it returns renders too — whether or not their props changed.
The last point surprises people: "props changed" is not the trigger.
A child re-renders because its parent did; memo (lesson 12) is the opt-in
that makes a child skip when its props are equal.
How React matches the new tree to the old
Comparing two arbitrary trees optimally is expensive, so React uses two cheap rules:
- Elements are matched by position among their siblings (or by
key, next lesson). - At a matched position, if the element type is the same, React keeps the existing component instance — and its state — and just gives it new props. If the type differs, React unmounts the old subtree, throwing away all state inside it, and mounts the new one from scratch.
The model below applies those rules to a few tree changes:
// Walk both trees together: a node keeps its instance (and state) only// when it has the same type at the same position and its parent was kept.function diff(prev, next, path = "root", out = []) { if (!next) return out; const kept = prev != null && prev.type === next.type; out.push(`${path} ${next.type}: ${kept ? "kept" : "mounted fresh"}`); (next.children ?? []).forEach((child, i) => diff(kept ? prev.children?.[i] : null, child, `${path}.${i}`, out), ); return out;} const counter = { type: "Counter" };export function run() { return { ternarySameSlot: diff( { type: "div", children: [counter] }, // isFancy ? <Counter fancy /> ... { type: "div", children: [{ type: "Counter" }] }, // ... : <Counter /> ), wrapperChanged: diff( { type: "div", children: [counter] }, { type: "section", children: [counter] }, ), slotKeptByNull: diff( { type: "div", children: [{ type: "Banner" }, counter] }, { type: "div", children: [null, counter] }, // {show && <Banner />} ), typeSwapped: diff( { type: "div", children: [counter] }, { type: "div", children: [{ type: "Timer" }] }, ), };}Wrapping a component in a different parent (div to section) resets it
even though the component itself did not change, because its parent was
replaced and the whole subtree went with it. Conversely, two branches of a
ternary that both render <Counter /> in the same slot share one
instance: switching branches keeps the count.
Do not define components inside components
The type React compares is the function itself. A component declared inside another component's body is a brand-new function on every render, so its type never matches the previous one:
function Field() { return "input";} function Parent() { // Declared inside: a new function object every time Parent runs. function InnerField() { return "input"; } return { outer: Field, inner: InnerField };} export function run() { const first = Parent(); // render 1 const second = Parent(); // render 2 return { outerSameType: Object.is(first.outer, second.outer), innerSameType: Object.is(first.inner, second.inner), };}Every render of the parent unmounts and remounts the inner component, losing its state and any focused input inside it. Declare components at the top level of the module and pass data through props.
Common mistakes
- Believing a child renders only when its props change. It renders
whenever its parent renders, unless it is wrapped in
memoand its props are shallowly equal. - Declaring a component inside another component. A new function type each render means remount each render, as the second example shows; typing into an input inside it loses focus after every keystroke.
- Changing a wrapper element conditionally, such as
isWide ? <section><Form /></section> : <div><Form /></div>. The wrapper's type changes, soFormloses its state. - Causing side effects during render. Rendering can happen more often than you expect (and twice in Strict Mode); work that touches the outside world belongs in handlers or effects.
What interviewers ask
- What is reconciliation? — Comparing the new element tree with the previous one to work out the DOM changes, using cheap heuristics rather than an optimal diff: match by position or key, and treat a different type as a different subtree.
- Does a child re-render when the parent renders but the child's props
did not change? — Yes, by default;
memomakes it skip when props are shallowly equal. - Why does my input lose focus on every keystroke? — Usually its
component is defined inside another component (new type each render)
or its
keychanges, so React remounts it.
Exercise · Pro
Which state survives a re-render
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade