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:
| Concept | What it is | When it matters |
|---|---|---|
| React element | Immutable { type, props, key } object | Every render produces a new description |
| Fiber tree | Mutable internal work structure | Hooks, effects, and interruptible render |
| Render phase | Build next tree, diff — no DOM writes | Can be paused or discarded |
| Commit phase | Apply DOM mutations, run layout effects | Synchronous, never interrupted |
| Reconciliation | Diff old vs new tree by type and key | Identity, 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
↓
PaintRender 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 differenceConsole 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 entirelyType 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 destroyedtype 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}beatsuseEffect(() => 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 continueThis 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 contentHigher-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 treeInterview: 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:
memoguards the child, not the parent. Tearing from external reads is fixed byuseSyncExternalStore, 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