Topics
v5.0.0
On this page · sections

Rendering, Reconciliation & Fiber

React keeps a lightweight in-memory description of the UI and derives the smallest set of DOM operations from it. Fiber is the reconciler that makes that process interruptible:

ConceptWhat it isWhen it matters
React elementImmutable { type, props, key } objectEvery render produces a new description
Fiber treeMutable internal work structureHooks, effects, and interruptible render
Render phaseBuild next tree, diff — no DOM writesCan be paused or discarded
Commit phaseApply DOM mutations, run layout effectsSynchronous, never interrupted
ReconciliationDiff old vs new tree by type and keyIdentity, list order, bailout rules

React Elements & the Virtual DOM

Definition

A React element is a plain JavaScript object describing what should appear on screen. It is cheap to create; touching the real DOM is not.

const element = <button type="button">Count: 0</button>; // Roughly equivalent to: const sameThing = { type: 'button', key: null, props: { type: 'button', children: 'Count: 0' }, };

Elements are not DOM nodes. React compares element trees and only writes to the DOM when the diff finds a difference.

Real-world example

'use client'; import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <button type="button" onClick={() => setCount((c) => c + 1)}> Count: {count} </button> ); }

Each click creates a new element object. React reconciles it against the previous one, and the only DOM write is a single text-node update.

Interview: React elements are immutable descriptions; the Fiber tree is the mutable internal structure React uses to reconcile them.

Render vs Commit

Definition

Every state update follows two distinct phases:

setState ↓ Render — call components, build next fiber tree, diff ↓ Commit — mutate DOM, run layout effects ↓ Paint

Render phase is pure and interruptible — no DOM mutation. Commit phase is synchronous and runs to completion.

Component function runs → always on re-render DOM node changes → only if the diff finds a difference

Console order on mount: render → layout effect → passive effect.

Interview: Concurrent rendering changed scheduling, not diffing rules. Render can be interrupted; commit cannot.

Reconciliation Rules

Definition

React applies heuristics in linear time — not optimal tree edit distance:

1. Different element type at same position → unmount old subtree, mount new one (state destroyed) 2. Same type, no key concern → keep fiber, update props in place, recurse into children 3. Sibling lists → match by (key, type) → matched: reuse fiber, move DOM if order changed → unmatched old: unmount; unmatched new: mount 4. Bailout → React.memo shallow compare passes, no pending state/context → skip subtree entirely

Type change example

// Before <div><Editor /></div> // After — Editor unmounts and remounts, losing state <span><Editor /></span>

Swapping element types at the same position throws away the subtree rather than transplanting children.

Interview: Reconciliation is O(n) over the dirty subtree. Keys turn positional matching into explicit identity.

Keys & List Identity

Definition

Keys tell the reconciler which child instance corresponds to which item across renders. React matches siblings by (key, type):

Same key + same type → reuse fiber, update props, move DOM if order changed Different key or type → unmount old, mount new — state and refs destroyed
type Todo = { id: string; text: string }; function TodoList({ todos }: { todos: Todo[] }) { return ( <ul> {todos.map((todo) => ( <li key={todo.id}> <input defaultValue={todo.text} aria-label={todo.id} /> </li> ))} </ul> ); }

Key is not a prop — child components never receive key. Pass id explicitly if the child needs it.

The index key bug

'use client'; import { useState } from 'react'; function Demo() { const [items, setItems] = useState(['A', 'B']); return ( <> <button type="button" onClick={() => setItems((x) => ['NEW', ...x])}> Prepend </button> {items.map((item, i) => ( <input key={i} defaultValue={item} /> ))} </> ); }

After prepend, the first input shows A, not NEW — the fiber at index 0 was reused and defaultValue does not reset on an existing DOM node.

Index keys are acceptable only when length, order, and row state are all static (e.g. weekday headers). Virtualized lists always need stable record ids.

Interview: Keys define identity, not performance. Wrong keys attach wrong state to wrong rows — a correctness bug.

Intentional Remount via Key Change

Definition

Changing a component’s key tells React it is a different instance — state, refs, and effects reset on mount.

<Editor key={docId} docId={docId} />

Without key={docId}, switching docs keeps the previous draft in state. Never use key={Date.now()} — remounts every parent render.

Interview: key={entityId} beats useEffect(() => setState(prop), [prop]) for entity switches.

Why Fiber Exists

Definition

The stack reconciler (React 15 and earlier) walked the tree recursively on the JS call stack. Once started, it ran to completion — blocking input, animation, and paint.

Fiber models reconciliation as a linked list of work units with a custom work-in-progress pointer:

Stack reconciler → one sync pass, blocks main thread Fiber work loop → process unit → shouldYield? → pause or continue

This enables concurrent rendering, Suspense, transitions, and selective hydration.

'use client'; import { useState, useTransition } from 'react'; function App() { const [text, setText] = useState(''); const [query, setQuery] = useState(''); const [pending, startTransition] = useTransition(); return ( <> <input value={text} onChange={(e) => { setText(e.target.value); startTransition(() => setQuery(e.target.value)); }} /> {pending && <span>Updating…</span>} <SearchResults query={query} /> </> ); }

Interview: Fiber is the reconciler, not the Virtual DOM concept. It still runs on the main thread — cooperative scheduling, not Web Workers.

Time Slicing & Lanes

Definition

The work loop processes fibers incrementally:

while (workInProgress && !shouldYield()) { performUnitOfWork(); }

Lanes are a bitmask — each bit group represents a priority class for updates on a root:

Sync / discrete input → clicks, key presses, typing Transition → startTransition, useDeferredValue Idle / offscreen → prerendering, hidden content

Higher-priority updates interrupt lower-priority render work. In-progress work is abandoned and restarted with fresh state.

setText(value); // sync — input startTransition(() => setFilter(value)); // transition — expensive tree

Interview: Transitions manage latency; they do not make render cheaper. A 400ms render still takes 400ms — it just does not block input.

Render Purity & Re-render Triggers

Definition

Render may be discarded and retried — component bodies must be pure: no prop/state mutation, no side effects, no assumption a render commits. Strict Mode double-invokes in dev.

Re-render triggers: own state, context identity change, parent render, external store subscription. React.memo and reference-equal elements allow bailouts. React 18+ batches everywhere.

Interview: memo guards the child, not the parent. Tearing from external reads is fixed by useSyncExternalStore, not transitions.

Senior Interview Summary

React element

Immutable { type, props, key } object. Cheap to create; describes UI at a point in time.

Fiber

Mutable internal node holding hooks, effects, and pending work. Enables interruptible render.

Render phase

Pure, no DOM writes, can be paused or discarded. Strict Mode double-invokes in dev to catch impure logic.

Commit phase

Synchronous DOM mutation plus layout effects. Never interrupted.

Reconciliation & keys

Type comparison, key matching, bailouts. O(n) heuristic — not minimal edit distance. Stable keys preserve identity across reorder.

Best mental model

Elements → "What should the UI look like?" Fiber → "How does React track and schedule work?" Render → "Compute the next description." Commit → "Write the diff to the DOM." key → "Which instance is this?"

Practical rule

Declarative UI at scale → Virtual DOM + reconciliation Wrong state on reordered row → fix keys, not memo Typing feels laggy → split urgent/deferred state + startTransition External store in render → useSyncExternalStore

Last updated on