Senior Frontend Depth
React Rendering Model: Render, Commit, Reconciliation and Keys
What “render” really means, how React finds what changed between renders, why keys matter and how batching works.
On this page
In one line: React works in two phases. Render calls your components to describe what the UI should look like. It is pure calculation with no side effects. Commit makes the smallest possible DOM changes and runs effects. Between them, reconciliation compares the new description with the old one. It uses the element type and key to decide what to keep, update or throw away.
Many "weird React bugs" go away once you can say exactly what happens in each phase. Examples are state in the wrong row, old values, and repeated work. This lesson builds the model that interviewers ask about.
What "render" means
A component is a function that returns a description of part of the screen. Props are the inputs a parent gives it, state is data it remembers between renders (a hook such as useState is a function that lets a component use React features like state), and context is a value shared with all components below a provider.
Rendering means React calls your component functions to find out what to show. It does not mean "update the screen." It does not always touch the DOM.
Trigger ──▶ Render (call components, build the new element tree) ──▶ Commit (update the DOM, run layout effects, then effects)- Trigger: this is the first render (
root.render(<App />)) or a state update. A state update can besetState, a context change, or a store update that re-renders a component that listens to it. - Render phase: React calls the component whose state changed. It also calls every child component below it. There are two exceptions. A child can be skipped by
memo(React.memowraps a component so React skips it when its props are the same as last time), or by the "same element" optimisation (Debugging, Lesson 03). The result is a tree of React elements. An element is a small plain object that describes one piece of UI, like{ type: "li", props, key }. The DOM (Document Object Model) is the browser's live tree of the real page elements. - Reconciliation: React compares the new element tree with the old one to find what changed.
- Commit phase: React applies the changes to the DOM, attaches refs, and runs effects.
Render ≠ DOM update. A component can re-render and return the same output. Then React changes nothing in the DOM. A wasted render only costs the function call and the comparison. It does not cost a repaint.
Rendering must be pure
A pure function always gives the same output for the same input, and it changes nothing outside itself. Think of a calculator. To mount a component means React creates it and adds it to the screen for the first time. To unmount means React removes it.
During the render phase, your component must act like a pure function of its props, state and context. That means the same inputs give the same output, and there are no side effects. A side effect is anything that reaches outside the component. For example: changing an outside variable, a network call, a subscription, or using Math.random() or Date.now() in the output.
Why? React may call render more than once, pause it, throw it away, or restart it. This happens in concurrent rendering (React can pause a render and continue later, see below), and in Strict Mode (a wrapper that runs extra checks in development, such as rendering each component twice). Impure render code then gives repeated or inconsistent results.
// ❌ Mutates something outside the component during renderlet count = 0;function Badge() { count++; return <span>{count}</span>; } // double render in Strict Mode: shows 2, then 4…// ✅ Pure: derived only from its inputsfunction Badge({ count }: { count: number }) { return <span>{count}</span>; }Side effects belong in event handlers (they run when the user does something) or in effects (they keep your component in sync with the outside world). See Lesson 01.
State is a snapshot
Each render sees a fixed snapshot of state, props and handlers. A snapshot is like a photo: it does not change after it is taken. Calling setState does not change the variable in the current render. It asks React for a new render with the new value.
function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); setCount(count + 1); setCount(count + 1); console.log(count); // still 0: the snapshot of this render } // result after one click: count is 1, not 3 — all three calls used count = 0 function handleClickRight() { setCount((c) => c + 1); // updater function: "take the latest pending value and add 1" setCount((c) => c + 1); setCount((c) => c + 1); // result: 3 } return <button onClick={handleClickRight}>{count}</button>;}Rule: if the next state depends on the previous state, use the updater form (setX(prev => …)).
Batching
Batching means React groups several state updates into one re-render. It does this for updates in the same event. Since React 18, it also does this inside timeouts, promises and native handlers. This is called "automatic batching".
async function save() { setSaving(true); setError(null); await api.save(); setSaving(false); // updates after an await are still batched with each other (React 18+) setSaved(true);}Batching gives fewer renders and a UI that stays consistent. You never see saving=false with saved=false for one frame. In rare cases you need the DOM to update right away. Then flushSync(() => setState(...)) (a function from react-dom that makes React apply the updates inside it to the DOM synchronously, right now) forces an immediate update. It costs some performance.
Reconciliation: how React decides what changed
Reconciliation is the step where React compares the new element tree with the old one and works out the smallest set of changes. Finding the perfect answer for two general trees is very slow (the best known algorithms take roughly cubic time, O(n³)). So React uses two heuristics (quick rules of thumb that work well in practice):
- Elements of different types produce different trees. Say a
<div>becomes a<section>, or<Profile>becomes<Settings>. Then React destroys the old subtree and builds a new one. State inside it is lost, and effects clean up and run again. - Keys identify children across renders. In a list, an element's
keytells React "this is the same item as last time", even if it moved.
Same type at the same position: update, keep state
// Render 1: <Counter /> Render 2: <Counter /> (same type, same position)// → React UPDATES the existing instance: its state is preserved.State is not stored "inside the component function." It is attached to the position of the component in the tree (to its Fiber node, the object React keeps for each component instance). Suppose a component of the same type renders at the same position. Then its state stays, even if you think of it as "a different thing".
function Page({ userId }: { userId: string }) { return <Profile />; // same type, same position for every userId}// <Profile> keeps its state (a half-typed form) when userId changes. Often a bug!// Fix: change the key to reset state when the identity changes<Profile key={userId} /> // new key → React treats it as a different instance → fresh stateA key is a special prop (key="...") that gives an element a stable identity. Using key as a reset button is the normal way to throw away state when the thing you show changes. Examples: a form for a different user, or a different video.
Different type at the same position: reset
{isEditing ? <EditForm /> : <ReadOnlyView />} // different types: switching unmounts one and mounts the otherDon't define components inside components
function Parent() { function Child() { return <input />; } // ❌ a NEW function (new type) on every render return <Child />; // React sees a different type each time → remounts → input loses focus/state}Define components at module level (at the top level of the file).
Keys in lists
{todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)}A good key is stable: the same item has the same key on every render. It is also unique among its siblings. Use a database ID. Never use Math.random(). A new key on each render means React rebuilds everything.
Why index keys are risky
{todos.map((todo, index) => <TodoItem key={index} todo={todo} />)}An index key names a position, not an item. Index keys are fine only for static lists that never change order and never get new or removed items. Otherwise they cause bugs.
Scenario: after you delete a list item, its state "jumps" to another row.
Items: [A, B, C] keys by index: 0:A 1:B 2:C Each row has an uncontrolled <input> (the browser, not React, holds its text).User types "hello" into B's input (index 1).User deletes A. New list: [B, C] keys by index: 0:B 1:CReact compares by key: key 0 (was A) → now B: same type, so it UPDATES the instance, keeping the old input value key 1 (was B, holding "hello") → now C: keeps "hello"! ← C appears to have B's text key 2 → removed.The state stayed attached to the position, not to the item. Now use key={todo.id}. React matches the instance for B to B, wherever B moves. The instance for A is destroyed. The same problem causes wrong animations, lost focus, and wrong memoisation (saved results) when a list changes order.
What happens in the commit phase
After render and reconciliation, React commits in this order:
- Mutation: React applies the DOM changes (insert, update, remove nodes). A ref is a box from
useRefthat can point to a DOM node. React detaches old refs here, and attaches new refs once the nodes exist, just before layout effects run. - Layout effects (
useLayoutEffect) run synchronously (right away, before anything else). This is after the DOM is updated but before the browser paints. Use them to measure the DOM or adjust the layout without a visible flicker. They block painting, so keep them very small. - The browser paints.
- Passive effects (
useEffect) run after paint in most cases. (An effect is code that syncs the component with something outside React; see Lesson 01. To paint means the browser draws the page on screen.) So they do not delay what the user sees.
useLayoutEffect(() => { const h = ref.current!.getBoundingClientRect().height; // read layout right after DOM mutation setHeight(h); // causes a synchronous re-render before paint}, []);useEffect(() => { const id = subscribe(onChange); // synchronise with an external system after paint return () => unsubscribe(id); // cleanup runs before the next effect and on unmount}, []);Cleanup order: when dependencies change, React first runs the cleanup of the previous effect. Then it runs the new effect. When the component unmounts, React runs the cleanups.
Inside React: Fiber and concurrent rendering
React represents your tree as Fiber nodes. Each Fiber is one unit of work. In concurrent rendering, the render phase can be interrupted. React can render in small chunks. It can pause so that urgent work (typing, clicking) goes first. Then it can continue, or throw away the unfinished work. A transition is an update you mark as non-urgent (with startTransition), so React can interrupt it.
The commit phase cannot be interrupted. Once React starts changing the DOM, it finishes. This is why render must be pure: it may run, stop, and run again. It is also why transitions work. See Lesson 02.
Putting it together: an interview-ready walk-through
"When state changes, React schedules a render. In the render phase, it calls my components, which must be pure. This gives a new tree of elements. By default, it re-renders the children too. Then React reconciles the new tree with the old one. Elements of a different type are torn down and rebuilt. An element of the same type at the same position is updated and keeps its state. Keys identify list items, so state follows the item. In the commit phase, React makes the smallest DOM changes. It runs layout effects before paint and passive effects after paint. Updates that happen in the same event are batched into one render."
Common mistakes
- Believing a re-render means a DOM update, and then over-optimising.
- Impure render. This means changing variables, reading
Date.now()or random values, or fetching data during render. - Index (or random) keys on lists that change.
- Defining components inside other components. This causes remounts.
- Expecting
setStateto update the variable at once, and not using the updater form. - Forgetting that state stays when type and position are the same. Then not using
keyto reset it. - Reading layout in
useEffect, which causes a flash. UseuseLayoutEffectinstead. The opposite mistake is heavy work inuseLayoutEffect, which blocks paint.
Interview questions
Render phase vs commit phase?
The render phase calls components to work out the next UI description. It is pure and can be interrupted. Nothing is visible yet. The commit phase applies the smallest DOM changes, attaches refs, and runs layout effects (before paint) and passive effects (after paint). It runs to the end without being interrupted.
Why are index keys risky?
Keys tie state to a position instead of an item. When items are inserted, deleted or reordered, React matches instances by index. So the component state and DOM state (focus, input values) stay at the old position while the data moves. This causes wrong values and bugs. Use stable, unique IDs.
What does reconciliation compare?
It compares the old and new element trees, level by level. It uses two rules of thumb. Elements of different types produce different trees, so the old subtree is unmounted and the new one is mounted. Elements of the same type at the same position are updated in place. Keys let React match list children across renders.
Scenario: after deleting a list item, its state "jumps" to another row. Why?
The list uses index keys. After the delete, React matches instances by position. So each remaining row gets the local state (input text, expanded state) of the instance that was in that position before. If I use a stable ID as the key, the state stays attached to the item, and the bug is fixed.
How do you reset a component's state when a prop identity changes?
I give it a key made from that identity, like <Profile key={userId} />. A new key makes React treat it as a different instance. The old one unmounts, and a fresh one mounts with its starting state.
What does batching do?
It joins several state updates from the same event, timeout, promise or other source into one re-render. This avoids a UI that is briefly inconsistent, and it avoids wasted work. React 18 uses automatic batching almost everywhere. flushSync flushes the updates inside it at once, without waiting for the batch, when I need the DOM to update right away.
useLayoutEffect vs useEffect?
useLayoutEffect runs right after the DOM changes and before the browser paints. So it suits measuring and layout fixes that must not flicker. But it blocks painting. useEffect runs after paint. It is the default choice for syncing with external systems.
Why must render be pure?
React may call render many times, pause it, throw it away, or run it concurrently. Strict Mode also calls it twice on purpose in development. If render had side effects, they would be repeated or would leave inconsistent state. Side effects belong in event handlers and effects.
Scenario: when a sorted list reorders, every row's expand/collapse state stays at the old positions. Why?
The rows use index keys. So React matches instances by position and keeps the local state where it was, while the data moves. I would use a stable, unique ID as the key, so each item's state moves with it. The same fix also solves wrong animations and lost focus after a reorder.
Key takeaways
- Render means calling components (which must be pure) to describe the UI. Commit means applying DOM changes and running effects. Render is not a DOM update.
- State is a snapshot for each render. Use the updater form when the new value depends on the old one. Updates are batched.
- Reconciliation: a change of type resets state. The same type at the same position keeps state. Keys carry identity in lists. Use
keyto reset state on purpose. - Use stable, unique keys. Never use the index or a random value for lists that change. Never define components inside components.
- Layout effects run before paint, and passive effects run after. The render phase can be interrupted. The commit phase cannot.
What's next
Lesson 01: Hooks in Depth. Now the rendering model is clear. Next we look at the tools that attach state and effects to it. We cover how they work, their rules, stale closures (old values that a function remembers), and when not to use an effect.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- State and Rendering: What Really Happens When State Changes · React in Depth
- “The API Is Fast but the UI Is Slow”: How to Solve It · Scenario Debugging
- Frontend and React Scenario Bank · Scenario Debugging
Part of the study guide
- TypeScript, React and Next.js Guide for Developers
- React Guide: Components, Hooks, State and Performance
Related series