Skip to main content

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​

  1. Trigger: the initial render or a state update.
  2. 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.
  3. Commit phase: React applies the DOM changes, sets refs and runs layout effects, all in one synchronous pass. useEffect callbacks 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:

  1. 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.
  2. key identifies 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 key breaks 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 key on 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.

FieldHolds
type, keyWhat to render, as in the element
child, sibling, returnLinks to the first child, the next sibling and the parent
memoizedStateThe state: for a function component, the linked list of its hooks
alternateThe same fiber in the other tree (current ↔ work in progress)
lanesPriority of the pending updates. Lanes replaced the older expiration times
flagsWhat 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, Suspense and 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()} or key={index} for a dynamic list.

See Also​