How React Rendering and Reconciliation Work: Render Phase, Commit Phase, Keys and Fiber Explained

How React rendering and reconciliation work - CodingHumans

"What happens when you call setState?" is one of the most common React interview questions. It is also the question behind most real performance bugs. Once you understand how React renders and reconciles, you can predict when components re-render, why state sometimes resets, why keys matter, and when memoisation actually helps.

This post goes through the whole update cycle in plain words: what starts a render, what "rendering" really means, how React decides what to change in the DOM, and what Fiber, batching and concurrent rendering add on top.

React update pipeline: trigger, render, reconcile, commit
Every React update goes through the same stages. Only the commit stage touches the real DOM.

1. What triggers a render

A component renders for one of four reasons:

  • Initial mount: createRoot(...).render(<App />) renders the whole tree once.
  • Its own state changes: you call a useState setter or a useReducer dispatch with a value different from the current one (React compares using Object.is).
  • Its parent renders: by default, when a component renders, React renders all its children too, whether their props changed or not.
  • A context it reads changes: every component that calls useContext(SomeContext) renders when the provider's value changes.

The third point surprises a lot of developers. A child does not render because its props changed. It renders because its parent rendered. Props only matter when you choose to skip renders with React.memo.

2. Render phase: what "rendering" really means

In React, "rendering" means calling your component function to find out what the UI should look like. The function returns React elements. These are plain JavaScript objects like { type: 'button', props: { onClick, children: 'Save' } }. Nothing is on screen yet.

Two rules follow from this:

  • Rendering must be pure. The same props and state should give the same output, with no side effects like API calls, subscriptions or DOM changes. React may call your component more than once, or start a render and throw it away. (Strict Mode calls it twice in development on purpose, to catch impure code.)
  • Rendering is not the same as updating the DOM. A component can render and give exactly the same output as before. Then React changes nothing on screen. A render is usually cheap. What you optimise are the expensive renders that didn't need to happen.

3. Reconciliation: how React diffs two trees

After rendering, React has two trees: the new one and the previous one. Comparing any two trees perfectly is an O(n³) problem, which is far too slow for UIs. So React uses an O(n) shortcut based on two assumptions:

  1. Elements of different types produce different trees. If a <div> becomes a <section>, or <ProfileCard> becomes <Spinner>, React throws away the old subtree completely (state is lost, effects are cleaned up) and mounts the new one from scratch.
  2. Keys identify children across renders. In a list of siblings, React matches old and new children by key. Without keys, it matches them by position.

When the type is the same, React keeps the existing DOM node or component instance, updates only the attributes or props that changed, and then moves on to the children. That is why state survives a re-render. React sees "same type in the same position" and keeps the instance.

// Same position, same type: state is kept
{isAdmin ? <Form role="admin" /> : <Form role="user" />}

// Different types: state is reset when isAdmin flips
{isAdmin ? <AdminForm /> : <UserForm />}

4. Keys and why index keys cause bugs

A key tells React "this is the same item as last time". With a stable id, adding an item at the top of a list is a single insert. With the array index as the key, every item after that point gets matched to a different key. So React updates every row, and any state inside those rows (an input's text, a checkbox, an open accordion) ends up on the wrong item.

Comparison of index keys and stable id keys when inserting into a list
Inserting at the top of a list: index keys shift every row, stable ids let React insert one.

Rules of thumb:

  • Use an id from your data (item.id). Index keys are safe only for lists that never reorder, filter or insert.
  • Don't generate keys during render (key={Math.random()} or crypto.randomUUID()). A new key on every render means React remounts every item every time.
  • You can use a key on purpose to reset state. <ChatWindow key={userId} /> gives each user a fresh chat window, so the previous user's draft doesn't carry over.

5. Commit phase and effects

Once React knows what changed, it commits. The commit phase is synchronous, and nothing can interrupt it:

  1. Mutation: React inserts, updates and removes DOM nodes.
  2. Layout effects: React attaches refs, then useLayoutEffect callbacks run synchronously, before the browser paints. Use them only when you must measure the DOM and adjust it before the user sees anything (for example, placing a tooltip).
  3. Paint: the browser draws the new frame.
  4. Passive effects: useEffect callbacks usually run after paint (for urgent events like a click, React may run them earlier). Before an effect runs again, React runs its previous cleanup function.

This order explains a classic interview puzzle. Logs from the component body come first (render), then useLayoutEffect, then useEffect. With a parent and child, the child's effects run before the parent's, because children finish committing before their parents.

6. Fiber, batching and concurrent rendering

Fiber

Fiber is the internal design React has used since version 16. Each component instance has a "fiber" object. It holds the type, props, state, effects, and links to the parent, child and sibling. React keeps two trees: the current tree on screen, and a work-in-progress tree it builds during render. On commit it swaps them (this is called double buffering).

The work is split into small units, one fiber at a time. So React can pause rendering, handle something more urgent like a keystroke, and then resume or restart. This is what makes the concurrent features possible.

Automatic batching

Since React 18, several state updates in the same event, timeout, promise or native handler are grouped into one render:

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // one render, not two
}

Before React 18, updates inside setTimeout or fetch().then() were not batched. Also remember that state is a snapshot. Reading count right after setCount still gives the old value. Use the updater form (c => c + 1) when the next value depends on the previous one.

Concurrent rendering

startTransition, useTransition and useDeferredValue let you mark an update as non-urgent. React renders it in the background, and can drop that render if a more urgent update comes in. So typing in a search box stays smooth while a large filtered list renders behind it. Suspense uses the same system to show a fallback while data or code loads.

Memoisation and the React Compiler

React.memo skips re-rendering a child when its props are shallowly equal. useMemo and useCallback keep object and function props stable, so memo can do its job. The React Compiler adds this kind of memoisation for you at build time, so in projects that use it you rarely write these by hand. Either way, measure with the React DevTools Profiler before you optimise.

7. Common mistakes

  • Defining a component inside another component. The inner function is a new type on every render. So React unmounts and remounts it each time, and you lose state and focus. Move it to the top level of the file.
  • Expecting React.memo to help when you pass new objects. style={{ margin: 8 }} or onClick={() => ...} creates a new reference on every render, so the shallow comparison fails.
  • Putting fast-changing values in a top-level context. Every consumer re-renders when the value changes. Split contexts, or pass stable values.
  • Using effects to derive state. If a value can be calculated from props or state, calculate it during render. Syncing it with useEffect costs an extra render.
  • Side effects in render. If you fetch or subscribe in the component body, it runs on every render, and twice in Strict Mode.
Key takeaways
  • Rendering means calling components. Committing means changing the DOM. They are separate steps.
  • A parent rendering re-renders its children by default. Props only matter with memo.
  • Same type in the same position keeps state. A different type or a different key resets it.
  • Use stable ids as keys. Use a changing key on purpose to reset a component.
  • Fiber makes rendering interruptible, which is what transitions and Suspense build on. Automatic batching comes from React 18's new root.

Interview questions and FAQ

What is the virtual DOM?

It is the tree of React elements (plain objects) that your components return. React compares the new tree with the previous one and applies only the differences to the real DOM. The current React docs say "rendering" and "reconciliation" instead of "virtual DOM", but interviewers still use the term.

Does calling setState with the same value cause a re-render?

If Object.is(newValue, oldValue) is true, React bails out and does not re-render the children. It may still call the component once before bailing out, but that does no harm.

What is the difference between useEffect and useLayoutEffect?

Both run after the DOM is updated. useLayoutEffect runs synchronously before paint and holds the paint back. useEffect runs after paint. Use useEffect unless you need to measure layout and avoid a visible flicker.

Why is it bad to use the array index as a key?

When items are added, removed or reordered, each index now points to a different item. So React reuses the wrong component instances and DOM nodes. You get extra DOM work, and state shows up on the wrong row.

What problem does Fiber solve?

The old stack reconciler rendered the whole tree in one pass that could not be stopped, and that could block the main thread. Fiber splits the work into small units, so React can prioritise, pause and resume rendering.

Comments

Answered 0/0Correct 0