Rendering and Reconciliation
Rendering is React calling components to get a tree of elements (the "virtual DOM"). Reconciliation is comparing this tree with the previous one to find the minimal set of DOM changes. An update has two phases: render (compute what changed; pure, can be interrupted) and commit (apply it to the DOM; synchronous). A re-render of a component does not mean that the DOM was touched.
Details
Render and Commit
- Trigger: the initial render or a state update.
- Render phase: React calls the component and, recursively, its children, and compares the result with the previous tree. Nothing is changed in the DOM. This work can be paused or thrown away.
- Commit phase: React applies the DOM changes, sets refs and runs layout
effects, all in one synchronous pass.
useEffectcallbacks run later, usually after the browser paints.
A component re-renders when:
- its state changed;
- its parent re-rendered (whether or not the props changed, unless it is wrapped
in
memo); - a context it reads changed.
Virtual DOM
- It is the tree of React elements: plain JavaScript objects that describe the UI.
- "The DOM is slow, the virtual DOM is fast" is a myth. A DOM operation is cheap; what is expensive is unnecessary layout and paint, and keeping the DOM in sync with the state by hand.
- What it gives: the UI is written as a function of state, and React works out the DOM updates. It also makes other renderers possible (React Native).
- It is not free: diffing takes time. Svelte and Solid work without a virtual DOM.
Diffing Heuristics
Comparing two arbitrary trees is O(n³). React does it in O(n) with two assumptions:
- Elements of different types produce different trees. If the type at a
position changed (
<div>→<span>,<A>→<B>), the old subtree is destroyed with its state and a new one is built. keyidentifies a child among its siblings. With keys React matches list items by key and moves them; without keys it matches them by index.
Consequences:
- The same type at the same position keeps the DOM node and the component state; only the changed attributes and props are updated.
- An index as a
keybreaks when the list is reordered, filtered or prepended to: state and DOM (input values, focus) stay with the position, not with the item. A key must be stable and unique among siblings, such as an ID from the data. - Changing the
keyon purpose resets a component. - Trees are compared level by level: a subtree moved to another parent is recreated.
Fiber
Fiber is the reconciler that React has used since version 16 (2017). A fiber is an object that React keeps for every component instance and DOM node: one unit of work.
| Field | Holds |
|---|---|
type, key | What to render, as in the element |
child, sibling, return | Links to the first child, the next sibling and the parent |
memoizedState | The state: for a function component, the linked list of its hooks |
alternate | The same fiber in the other tree (current ↔ work in progress) |
lanes | Priority of the pending updates. Lanes replaced the older expiration times |
flags | What to do in the commit phase: insert, update, delete |
- The links form a linked list, so React walks the tree in a loop instead of recursion and can stop after any fiber and continue later.
- React builds a work-in-progress tree next to the current one and swaps them at commit.
- This is the base for
concurrent rendering,
Suspenseand error boundaries.
Example
The type, the position and the key decide whether state is kept:
import { useState } from 'react';
function Field({ label }) {
const [text, setText] = useState('');
return (
<label>
{label}
<input value={text} onChange={(e) => setText(e.target.value)} />
</label>
);
}
export default function Form() {
const [isCompany, setIsCompany] = useState(false);
return (
<>
<button onClick={() => setIsCompany(!isCompany)}>Switch</button>
{/* Same type, same position: the text survives the switch */}
{isCompany ? <Field label="Company" /> : <Field label="Name" />}
{/* Different keys: the field is remounted and the text is reset */}
{isCompany ? (
<Field key="company" label="Company" />
) : (
<Field key="name" label="Name" />
)}
</>
);
}
Follow-up Questions
- Does a re-render always update the DOM? No. If the render output is the same, the commit phase changes nothing.
- Why is the commit phase synchronous? A partially applied update would show an inconsistent UI.
- Why is an index a bad
key? See the consequences above; it is acceptable only for a static list that is never reordered. - What is the difference between the virtual DOM and Fiber? The virtual DOM is the idea of describing the UI with objects; Fiber is the implementation of the reconciler that processes them.
Common Mistakes
-
"The virtual DOM is faster than the real DOM."
-
"A component re-renders when its props change": it re-renders when the parent does.
-
key={Math.random()}orkey={index}for a dynamic list.