Virtual DOMReconciliationDiffing AlgorithmReact InternalsKeys

Reconciliation & Virtual DOM

Understand how React efficiently updates the UI using the Virtual DOM and reconciliation. Master the diffing algorithm, the role of keys, and the internal process that makes React fast — essential knowledge for frontend interviews.

35 min read14 sections
01

Overview

React doesn't update the real DOM directly when state changes. Instead, it maintains a lightweight JavaScript representation of the UI called the Virtual DOM. When something changes, React creates a new Virtual DOM tree, compares it with the previous one, and calculates the minimal set of changes needed — a process called reconciliation.

The diffing algorithm at the heart of reconciliation uses clever heuristics to compare trees in O(n) time instead of the theoretical O(n³). This is what makes React feel fast — it batches and minimizes real DOM operations, which are the most expensive part of any UI update.

Understanding this process is critical for writing performant React apps and answering one of the most frequently asked frontend system design interview topics.

Why this matters

Interviewers expect you to explain how React updates the UI internally. Knowing reconciliation helps you understand why keys matter, why unnecessary re-renders are expensive, and how to optimize component trees.

02

Why Do We Need Virtual DOM?

To understand why the Virtual DOM exists, you need to understand why direct DOM manipulation is problematic at scale.

🐢

DOM is Slow

Every DOM operation (create, update, delete) triggers the browser's rendering pipeline — style recalculation, layout, and paint. These are expensive operations.

🔄

Frequent Updates

Modern UIs update constantly — user input, API responses, animations. Each update touching the DOM directly causes layout thrashing and jank.

📦

Batching is Hard

Manually tracking which DOM nodes changed and batching updates is complex and error-prone. The Virtual DOM automates this entirely.

direct-dom-problem.jsjavascript
// ❌ Direct DOM manipulation — each call triggers layout/paint
const list = document.getElementById("list");

// 1000 individual DOM operations = 1000 potential reflows
for (let i = 0; i < 1000; i++) {
  const li = document.createElement("li");
  li.textContent = `Item ${i}`;
  list.appendChild(li);  // Triggers reflow each time
}

// ✅ What React does internally:
// 1. Build the entire new UI in memory (Virtual DOM)
// 2. Diff against the previous Virtual DOM
// 3. Batch all changes into ONE DOM update
// Result: minimal reflows, smooth UI
AspectDirect DOMVirtual DOM (React)
Update strategyModify DOM nodes directlyDiff in memory, patch minimal changes
PerformanceEach change triggers reflow/repaintBatched updates, minimal DOM touches
Developer experienceManual tracking of what changedDeclarative — describe UI, React handles updates
ComplexityGrows with app sizeAbstracted away by the framework
ConsistencyEasy to create inconsistent statesUI always reflects current state

The real insight

The Virtual DOM isn't faster than the DOM itself — nothing is faster than a single, targeted DOM operation. The Virtual DOM is faster than naive full re-rendering. It automates the process of figuring out the minimum changes needed, which is what makes it valuable.

03

What is Virtual DOM?

The Virtual DOM is a plain JavaScript object tree that mirrors the structure of the real DOM. Each node in the Virtual DOM is a lightweight description of a UI element — its type, props, and children.

virtual-dom-structure.jsxjavascript
// This JSX:
<div className="card">
  <h2>Title</h2>
  <p>Description</p>
</div>

// Becomes this Virtual DOM object (simplified):
{
  type: "div",
  props: {
    className: "card",
    children: [
      {
        type: "h2",
        props: { children: "Title" }
      },
      {
        type: "p",
        props: { children: "Description" }
      }
    ]
  }
}

// React.createElement() is what JSX compiles to:
React.createElement("div", { className: "card" },
  React.createElement("h2", null, "Title"),
  React.createElement("p", null, "Description")
);

Virtual DOM vs Real DOM

PropertyReal DOMVirtual DOM
NatureBrowser API — actual rendered elementsPlain JavaScript objects in memory
SpeedSlow to update (triggers rendering pipeline)Fast to create and compare (just JS objects)
MemoryHeavy — each node has hundreds of propertiesLightweight — only stores type, props, children
UpdatesDirectly mutates the documentCreates new tree, diffs, then patches real DOM
Accessdocument.getElementById, querySelectorInternal to React — not directly accessible
real-vs-virtual.jsjavascript
// A real DOM node has ~250+ properties
const div = document.createElement("div");
console.log(Object.keys(div).length); // ~250 properties!

// A Virtual DOM node is just a plain object
const vNode = {
  type: "div",
  props: { className: "card" },
  children: []
};
// ~3 properties. Creating and comparing these is trivial.

How React uses the Virtual DOM

Every time state or props change, React calls your component's render function to produce a new Virtual DOM tree. It then compares this new tree with the previous one (diffing) and applies only the differences to the real DOM. This is the reconciliation process.

04

What is Reconciliation?

Reconciliation is React's process of updating the UI when state or props change. It's the algorithm that decides what changed between the old and new Virtual DOM trees and how to efficiently update the real DOM to match.

1

Trigger

A state update (setState, useState setter) or new props cause a component to re-render. React calls the component's render function to produce a new Virtual DOM tree.

2

Render Phase

React builds the new Virtual DOM tree by calling component functions/render methods. This is pure computation — no DOM mutations happen here.

3

Diffing

React compares the new Virtual DOM tree with the previous one, node by node. It identifies exactly which nodes were added, removed, or changed.

4

Commit Phase

React applies the minimal set of changes to the real DOM in a batch. This is the only phase that touches the actual DOM. Effects (useEffect) run after this.

reconciliation-trigger.jsxjavascript
function Counter() {
  const [count, setCount] = useState(0);

  // When setCount is called:
  // 1. React schedules a re-render
  // 2. Calls Counter() again → new Virtual DOM
  // 3. Diffs new VDOM with previous VDOM
  // 4. Finds: <span> text changed from "0" to "1"
  // 5. Updates ONLY that text node in the real DOM

  return (
    <div>
      <h1>Counter</h1>           {/* unchanged — skipped */}
      <span>{count}</span>       {/* changed — updated */}
      <button onClick={() => setCount(count + 1)}>
        Increment                {/* unchanged — skipped */}
      </button>
    </div>
  );
}

Key distinction: Render vs Commit

The render phase (building VDOM + diffing) is pure and can be paused or aborted. The commit phase (DOM updates) is synchronous and can't be interrupted. This separation is what enables React's Concurrent Mode features.

05

Diffing Algorithm

The diffing algorithm is the core of reconciliation. Comparing two arbitrary trees has O(n³) complexity — far too slow for UI updates. React uses two heuristics to reduce this to O(n):

🌳

Heuristic 1: Different Types = Different Trees

If the root element type changes (e.g., <div> → <span>, or <ComponentA> → <ComponentB>), React tears down the entire old tree and builds a new one from scratch. No deep comparison needed.

🔑

Heuristic 2: Keys Identify Stable Elements

Developers can hint which child elements are stable across renders using the 'key' prop. This lets React match children efficiently without comparing every permutation.

How the Diff Works

1. Element Type Comparison

React first compares the type of the root elements. The behavior differs based on whether the type changed or stayed the same.

type-comparison.jsxjavascript
// CASE 1: Same element type → update props only
// Old:
<div className="old" title="hello" />
// New:
<div className="new" title="hello" />
// React: keeps the same DOM node, only updates className

// CASE 2: Different element type → destroy and rebuild
// Old:
<div>
  <Counter />
</div>
// New:
<span>
  <Counter />
</span>
// React: destroys <div> and <Counter> entirely
//        (including Counter's state!)
//        builds new <span> and new <Counter> from scratch

// CASE 3: Different component type → destroy and rebuild
// Old:
<ComponentA data={items} />
// New:
<ComponentB data={items} />
// React: unmounts ComponentA, mounts ComponentB
//        even if they render identical output

Why O(n³) is impractical

The general tree diff algorithm compares every node with every other node in both trees, then finds the optimal edit sequence. For 1,000 nodes, that's 1 billion operations. React's O(n) approach handles 1,000 nodes in ~1,000 operations — a million times faster.

2. Props Comparison

When the element type is the same, React compares the props (attributes) and updates only the ones that changed.

props-diff.jsxjavascript
// Old Virtual DOM:
{
  type: "div",
  props: { className: "card", style: { color: "red" }, id: "main" }
}

// New Virtual DOM:
{
  type: "div",
  props: { className: "card active", style: { color: "blue" }, id: "main" }
}

// React's diff result:
// - className: "card" → "card active"     ← CHANGED, update
// - style.color: "red" → "blue"           ← CHANGED, update
// - id: "main" → "main"                   ← SAME, skip
//
// DOM operation: element.className = "card active"
//                element.style.color = "blue"

3. Child Reconciliation

When diffing children, React iterates over both lists simultaneously. This is where keys become critical — without them, React can only compare children by position.

child-reconciliation.jsxjavascript
// WITHOUT keys — React compares by position (index)

// Old:                    New:
<ul>                       <ul>
  <li>Apple</li>    →       <li>Cherry</li>   // position 0: text changed
  <li>Banana</li>   →       <li>Apple</li>    // position 1: text changed
</ul>                        <li>Banana</li>   // position 2: NEW node
                           </ul>

// React updates ALL three <li> elements
// Even though Apple and Banana just moved!

// WITH keys — React matches by identity
// Old:                         New:
<ul>                            <ul>
  <li key="a">Apple</li>  →      <li key="c">Cherry</li>  // NEW
  <li key="b">Banana</li> →      <li key="a">Apple</li>   // MOVED
</ul>                             <li key="b">Banana</li>  // MOVED
                                </ul>

// React: insert Cherry, move Apple and Banana
// Existing DOM nodes are reused — much more efficient
Diffing Algorithm (Pseudocode)text
function diff(oldNode, newNode) {
  // 1. Type changed → replace entire subtree
  if (oldNode.type !== newNode.type) {
    return { type: "REPLACE", newNode };
  }

  // 2. Same type → compare props
  const propChanges = diffProps(oldNode.props, newNode.props);

  // 3. Recursively diff children
  const childChanges = diffChildren(
    oldNode.children,
    newNode.children
  );

  return { type: "UPDATE", propChanges, childChanges };
}

function diffChildren(oldChildren, newChildren) {
  // With keys: match by key, detect moves/inserts/deletes
  // Without keys: compare by index position only
}
06

Role of Keys

Keys are the most misunderstood concept in React reconciliation. They tell React which elements are the same across renders, enabling efficient reordering, insertion, and deletion of list items.

Why Keys Are Needed

Without keys, React has no way to know that an element moved — it can only compare children by their position (index). This leads to unnecessary DOM mutations and can cause bugs with component state.

keys-problem.jsxjavascript
// ❌ WITHOUT keys — React compares by index

// Render 1: items = ["Alice", "Bob", "Charlie"]
<ul>
  <li>Alice</li>     {/* index 0 */}
  <li>Bob</li>       {/* index 1 */}
  <li>Charlie</li>   {/* index 2 */}
</ul>

// Render 2: items = ["Zara", "Alice", "Bob", "Charlie"]
// (Zara added at the beginning)
<ul>
  <li>Zara</li>      {/* index 0: was "Alice" → text changed */}
  <li>Alice</li>     {/* index 1: was "Bob" → text changed */}
  <li>Bob</li>       {/* index 2: was "Charlie" → text changed */}
  <li>Charlie</li>   {/* index 3: NEW node created */}
</ul>

// React updates ALL 4 elements!
// It doesn't know Alice/Bob/Charlie just shifted down.
// If these <li> had internal state (inputs, checkboxes),
// that state would be WRONG — attached to the wrong items.
keys-solution.jsxjavascript
// ✅ WITH keys — React matches by identity

// Render 1:
<ul>
  <li key="alice">Alice</li>
  <li key="bob">Bob</li>
  <li key="charlie">Charlie</li>
</ul>

// Render 2: (Zara added at the beginning)
<ul>
  <li key="zara">Zara</li>        {/* NEW — insert */}
  <li key="alice">Alice</li>      {/* SAME key — reuse, no update */}
  <li key="bob">Bob</li>          {/* SAME key — reuse, no update */}
  <li key="charlie">Charlie</li>  {/* SAME key — reuse, no update */}
</ul>

// React: creates ONE new <li> for Zara, moves the rest.
// Existing DOM nodes and their state are preserved.

Good Keys vs Bad Keys

Key StrategyExampleVerdictWhy
Unique IDkey={item.id}✅ BestStable, unique — React can always identify the element
Unique stringkey={item.email}✅ GoodWorks if the value is unique and doesn't change
Array indexkey={index}❌ BadBreaks when items are reordered, inserted, or deleted
Random valuekey={Math.random()}❌ WorstNew key every render — React destroys and recreates every element
index-key-bug.jsxjavascript
// ❌ Bug: Using index as key with stateful components

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Buy milk" },
    { id: 2, text: "Walk dog" },
    { id: 3, text: "Code review" },
  ]);

  const removeTodo = (id) => {
    setTodos(todos.filter(t => t.id !== id));
  };

  return (
    <ul>
      {todos.map((todo, index) => (
        // ❌ Using index as key
        <li key={index}>
          <input defaultValue={todo.text} />
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

// Problem: Delete "Buy milk" (index 0)
// Before: [0: "Buy milk", 1: "Walk dog", 2: "Code review"]
// After:  [0: "Walk dog", 1: "Code review"]
//
// React sees: key 0 text changed, key 1 text changed, key 2 removed
// The INPUT VALUES stay attached to their index positions!
// Result: "Walk dog" row shows "Buy milk" in the input 😱

// ✅ Fix: Use unique ID as key
// <li key={todo.id}> — React correctly removes the right element

When is index as key OK?

Using index as key is acceptable only when all three conditions are met: the list is static (never reordered), items are never inserted/deleted from the middle, and items have no internal state (no inputs, no checkboxes). In practice, just use unique IDs — it's always safe.

07

Reconciliation Step-by-Step

Here's the complete flow of what happens when you call setState or a useState setter, from trigger to pixels on screen.

1

State/Props Change

A setState call, context change, or parent re-render triggers the update. React marks the component as 'dirty' and schedules a re-render.

2

New Virtual DOM Created

React calls the component function (or render method) to produce a new Virtual DOM subtree. This is just JavaScript execution — no DOM is touched.

3

Diffing: Compare Old vs New VDOM

React walks both trees simultaneously, comparing node types, props, and children. It uses the O(n) heuristic algorithm with key-based matching for lists.

4

Identify Minimal Changes

The diff produces a list of operations: create node, update props, move node, delete node. This is the minimal set of mutations needed.

5

Commit: Update Real DOM

React applies all changes to the real DOM in a single synchronous batch. The browser then recalculates styles, layout, and paints the updated pixels.

6

Run Effects

After the DOM is updated, React runs useEffect callbacks and ref assignments. Cleanup functions from the previous render run first.

Reconciliation Flowtext
setState(newValue)


┌──────────────────────────────────┐
RENDER PHASE
│   (pure, can be paused/aborted) │
│                                  │
1. Call component function
│   2. Build new Virtual DOM tree
│   3. Diff old VDOM vs new VDOM
│   4. Collect list of changes
│                                  │
No side effects here!          │
└──────────────┬───────────────────┘


┌──────────────────────────────────┐
COMMIT PHASE
│   (synchronous, can't pause)    
│                                  │
│   5. Apply DOM mutations
│      - Insert new nodes
│      - Update changed props
│      - Remove deleted nodes
│                                  │
│   6. Run layout effects
│   7. Browser paints
│   8. Run passive effects
│      (useEffect callbacks)       │
└──────────────────────────────────┘

Batching in React 18+

React 18 automatically batches multiple state updates into a single re-render, even inside promises, timeouts, and event handlers. This means calling setState three times in a row produces only one reconciliation cycle, not three.

08

Code Examples

These examples show what React does internally during reconciliation. Understanding these patterns is essential for interviews and debugging performance issues.

Example 1: Simple State Update → DOM Update

example-1-state-update.jsxjavascript
function Profile({ user }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div className="profile">
      <h1>{user.name}</h1>
      {isEditing ? (
        <input defaultValue={user.bio} />
      ) : (
        <p>{user.bio}</p>
      )}
      <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing ? "Save" : "Edit"}
      </button>
    </div>
  );
}

// When setIsEditing(true) is called:
//
// Old VDOM:                    New VDOM:
// <div>                        <div>
//   <h1>John</h1>               <h1>John</h1>        ← same, skip
//   <p>Developer</p>            <input value="..."/>  ← TYPE CHANGED!
//   <button>Edit</button>       <button>Save</button> ← text changed
// </div>                       </div>
//
// React's diff:
// 1. <div> — same type, check children
// 2. <h1> — same type, same props → SKIP
// 3. <p> → <input> — DIFFERENT type → destroy <p>, create <input>
// 4. <button> — same type, text changed → update text node
//
// DOM operations: remove <p>, insert <input>, update button text

Example 2: List Without Keys (Problem)

example-2-no-keys.jsxjavascript
function TaskList() {
  const [tasks, setTasks] = useState(["Design", "Develop", "Test"]);

  const addTask = () => {
    setTasks(["Plan", ...tasks]); // Add to beginning
  };

  return (
    <ul>
      {tasks.map((task, index) => (
        // ❌ No key (React uses index by default)
        <li>{task} <input placeholder="notes" /></li>
      ))}
    </ul>
  );
}

// Before addTask():
// index 0: <li>Design  <input /></li>  (user typed "mockups" in input)
// index 1: <li>Develop  <input /></li>
// index 2: <li>Test     <input /></li>

// After addTask() — React compares by index:
// index 0: "Design" → "Plan"     ← UPDATE text (input stays!)
// index 1: "Develop" → "Design"  ← UPDATE text (input stays!)
// index 2: "Test" → "Develop"    ← UPDATE text
// index 3: (new) → "Test"        ← CREATE new <li>

// BUG: The input with "mockups" is still at index 0,
// but now it's next to "Plan" instead of "Design"!
// React updated 4 DOM nodes instead of inserting 1.

Example 3: List With Proper Keys (Optimized)

example-3-with-keys.jsxjavascript
function TaskList() {
  const [tasks, setTasks] = useState([
    { id: "t1", name: "Design" },
    { id: "t2", name: "Develop" },
    { id: "t3", name: "Test" },
  ]);

  const addTask = () => {
    setTasks([{ id: "t0", name: "Plan" }, ...tasks]);
  };

  return (
    <ul>
      {tasks.map((task) => (
        // ✅ Unique, stable key
        <li key={task.id}>
          {task.name} <input placeholder="notes" />
        </li>
      ))}
    </ul>
  );
}

// Before addTask():
// key="t1": <li>Design  <input /></li>  (user typed "mockups")
// key="t2": <li>Develop  <input /></li>
// key="t3": <li>Test     <input /></li>

// After addTask() — React matches by key:
// key="t0": (no match) → CREATE new <li>Plan</li>
// key="t1": matched    → REUSE, no changes needed
// key="t2": matched    → REUSE, no changes needed
// key="t3": matched    → REUSE, no changes needed

// Result: 1 DOM insertion. Input with "mockups" stays with Design.
// 4x fewer DOM operations. State is preserved correctly.

What React does internally with keys

React builds a Map of old children keyed by their key prop. For each new child, it looks up the key in the map. If found, it reuses the existing fiber/DOM node. If not found, it creates a new one. Old keys not present in the new list are deleted. This is why keys must be unique among siblings.

09

Performance Insights

The Virtual DOM improves performance by minimizing real DOM operations, but it's not a silver bullet. Understanding when it helps and when it doesn't is key to writing fast React apps.

✓ Done

Use Stable, Unique Keys for Lists

Keys let React reuse existing DOM nodes when list items are reordered, inserted, or deleted. Without proper keys, React may recreate the entire list on every update.

✓ Done

Minimize Re-renders with React.memo

React.memo skips re-rendering a component if its props haven't changed. This prevents unnecessary Virtual DOM creation and diffing for subtrees that didn't change.

✓ Done

Keep Component Trees Shallow

Deep component trees mean more nodes to diff. Flatter structures reduce reconciliation work. Avoid unnecessary wrapper components that add depth without value.

✓ Done

Avoid Creating New Objects/Arrays in Render

Passing new object/array literals as props ({}, []) defeats React.memo because they fail referential equality checks every render. Use useMemo or hoist constants.

→ Could add

Use Virtualization for Long Lists

For lists with 1000+ items, render only visible items using react-window or react-virtuoso. This reduces the Virtual DOM tree size dramatically — fewer nodes to diff.

→ Could add

Colocate State Near Where It's Used

State in a parent causes all children to re-render. Moving state down to the component that actually uses it limits the reconciliation scope to a smaller subtree.

When Virtual DOM Doesn't Help

ScenarioWhy VDOM Doesn't HelpBetter Approach
Large flat lists (10k+ items)Diffing 10k nodes is still expensive even at O(n)Virtualization — only render visible items
Frequent, small updates (animations)VDOM overhead per frame adds up at 60fpsCSS animations, requestAnimationFrame, or refs for direct DOM
Unnecessary re-rendersVDOM is created and diffed even if nothing changedReact.memo, useMemo, useCallback to skip re-renders
Canvas/WebGL renderingVDOM can't represent canvas pixelsUse refs and imperative canvas API directly

The real performance win

The Virtual DOM's biggest value isn't raw speed — it's developer productivity with good-enough performance. You write declarative code describing what the UI should look like, and React figures out the minimal DOM operations. For the 95% case, this is fast enough. For the 5% that needs optimization, React gives you escape hatches (memo, refs, virtualization).

10

Common Mistakes

These mistakes cause real performance issues and bugs in production React apps. They're also common interview discussion points.

🔢

Using array index as key

Index keys break when items are reordered, inserted, or deleted. React can't distinguish between 'item moved' and 'item content changed,' leading to incorrect state and unnecessary DOM updates.

Always use a unique, stable identifier (database ID, UUID) as the key. Generate IDs at creation time if needed.

🔄

Unnecessary re-renders from parent

When a parent re-renders, ALL children re-render by default — even if their props didn't change. This triggers reconciliation on subtrees that produce identical Virtual DOM.

Use React.memo for expensive child components. Colocate state to the lowest component that needs it. Use useMemo/useCallback for object/function props.

🧠

Assuming Virtual DOM = always fast

The Virtual DOM adds overhead — creating JS objects, diffing trees, then applying changes. For simple, targeted updates, direct DOM manipulation can be faster.

Understand that VDOM is a trade-off: developer experience and correctness for a small performance cost. Use refs for performance-critical direct DOM access.

🎲

Using Math.random() or Date.now() as key

Random keys change every render, so React thinks every element is new. It destroys and recreates the entire list on every update — the worst possible performance.

Keys must be stable across renders. Use IDs from your data. If no ID exists, generate one when the item is created (not during render).

🏗️

Changing component type unnecessarily

Switching between component types (e.g., conditionally rendering ComponentA vs ComponentB at the same position) causes React to unmount and remount, destroying all state.

Keep the same component type and use props/state to change behavior. If you need to reset state intentionally, change the key instead.

📦

Creating new object/array props inline

Passing style={{color: 'red'}} or data={[1,2,3]} creates new references every render. React.memo can't optimize because props always look 'changed.'

Hoist constant objects outside the component. Use useMemo for computed objects. Use useCallback for function props.

11

Advanced Insights

These topics go beyond basic reconciliation. They're great for senior-level interviews and understanding React's evolution.

Fiber Architecture

React 16 introduced Fiber — a complete rewrite of the reconciliation engine. The old "stack reconciler" processed the entire tree synchronously and couldn't be interrupted. Fiber breaks work into small units that can be paused, resumed, and prioritized.

📚

Stack Reconciler (Pre-React 16)

Recursive, synchronous tree traversal. Once started, it had to finish the entire tree before yielding to the browser. Long updates blocked rendering and user input.

🧵

Fiber Reconciler (React 16+)

Each node is a 'fiber' — a unit of work with links to parent, child, and sibling. React can pause between fibers, handle urgent updates first, and resume later.

fiber-node-structure.jsjavascript
// Simplified Fiber node structure
const fiber = {
  // Identity
  type: "div",           // Element type (string or component)
  key: null,             // Key for reconciliation

  // Tree structure (linked list, not recursive tree)
  child: fiberChild,     // First child fiber
  sibling: fiberSibling, // Next sibling fiber
  return: fiberParent,   // Parent fiber

  // State
  stateNode: domNode,    // Reference to real DOM node
  memoizedProps: {},     // Props from last render
  memoizedState: {},     // State from last render
  pendingProps: {},      // New props for this render

  // Effects
  effectTag: "UPDATE",   // What to do in commit phase
  // "PLACEMENT" | "UPDATE" | "DELETION"

  // Work scheduling
  lanes: 0b0001,         // Priority lanes (binary)
  alternate: oldFiber,   // Link to previous fiber (double buffering)
};

Concurrent Rendering

Built on Fiber, concurrent rendering (React 18+) allows React to prepare multiple versions of the UI simultaneously. It can start rendering an update, pause to handle a more urgent one (like user input), and resume later.

  • startTransition — marks updates as non-urgent, letting React interrupt them for user input
  • useDeferredValue — shows stale content while new content renders in the background
  • Suspense — pauses rendering while data loads, shows fallback UI, resumes when ready
  • Automatic batching — groups all state updates into one render, even in async code
concurrent-features.jsxjavascript
import { startTransition, useDeferredValue } from "react";

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  // Urgent: update input immediately (user sees typing)
  // Non-urgent: update results list (can be interrupted)
  const handleChange = (e) => {
    setQuery(e.target.value);  // Urgent — renders immediately

    startTransition(() => {
      setResults(search(e.target.value));  // Non-urgent
      // React can interrupt this render if user types again
    });
  };

  // Alternative: useDeferredValue
  const deferredQuery = useDeferredValue(query);
  // deferredQuery lags behind query during rapid updates
  // React renders with the old value first (fast),
  // then re-renders with the new value in the background

  return (
    <>
      <input value={query} onChange={handleChange} />
      <ResultsList query={deferredQuery} />
    </>
  );
}

Fiber enables everything

Fiber's interruptible rendering is what makes concurrent features possible. Without it, React couldn't pause a low-priority update to handle user input. The reconciliation algorithm is the same O(n) diff — Fiber just changes when and how that work is scheduled.

12

Interview Questions

These are the most commonly asked reconciliation and Virtual DOM questions in frontend interviews. Practice explaining each one clearly and concisely.

Q:What is the Virtual DOM and why does React use it?

A: The Virtual DOM is a lightweight JavaScript object tree that represents the UI structure. React uses it as an intermediary — instead of updating the real DOM directly (which is slow), React builds a new Virtual DOM, diffs it against the previous one, and applies only the minimal changes to the real DOM. This batching and minimization of DOM operations is what makes React efficient.

Q:How does reconciliation work in React?

A: When state or props change, React re-renders the component to produce a new Virtual DOM tree. It then compares (diffs) this new tree with the previous one using an O(n) algorithm. The diff identifies which nodes were added, removed, or changed. Finally, React commits only those changes to the real DOM in a batch. This two-phase process (render + commit) ensures minimal DOM mutations.

Q:Why are keys important in React lists?

A: Keys give React a way to identify which elements in a list are the same across renders. Without keys, React compares children by index position — if you insert an item at the beginning, every item appears 'changed.' With unique keys, React can match elements by identity, reuse existing DOM nodes, preserve component state, and only create/remove what actually changed.

Q:What is the diffing algorithm and why is it O(n)?

A: React's diffing algorithm compares two Virtual DOM trees using two heuristics: (1) elements of different types produce different trees — React destroys the old subtree entirely, and (2) keys identify stable elements in lists. These heuristics avoid the O(n³) cost of a general tree diff by never comparing nodes across different levels or types. React walks the tree once, making it O(n).

Q:What is the difference between Virtual DOM and Real DOM?

A: The Real DOM is the browser's actual document tree — each node has hundreds of properties and updating it triggers style recalculation, layout, and paint. The Virtual DOM is a plain JavaScript object with just type, props, and children. It's fast to create and compare because it's just in-memory JS. React uses the Virtual DOM to calculate changes, then applies them to the Real DOM in a minimal batch.

Q:What happens when you change the element type during reconciliation?

A: When the element type changes (e.g., <div> to <span>, or <ComponentA> to <ComponentB>), React destroys the entire old subtree — unmounts components, removes DOM nodes, clears state. It then builds the new subtree from scratch. This is React's first heuristic: different types = different trees. No deep comparison is attempted.

Q:Why is using array index as a key problematic?

A: Index keys break when items are reordered, inserted at the beginning, or deleted. React matches by key — if you delete item 0, the old key=1 now matches new key=0, so React thinks the content changed instead of recognizing a deletion. This causes unnecessary DOM updates and, worse, component state (like input values) gets attached to the wrong items.

Q:What is React Fiber and how does it relate to reconciliation?

A: Fiber is React's reconciliation engine (introduced in React 16). It represents each component as a 'fiber' node in a linked list, allowing React to break rendering work into small units. Unlike the old stack reconciler that processed the entire tree synchronously, Fiber can pause, resume, and prioritize work. This enables concurrent features like startTransition and Suspense.

Q:How does React batch state updates?

A: In React 18+, all state updates are automatically batched — multiple setState calls in the same event handler, promise, or timeout produce only one re-render. React collects all updates, then runs a single reconciliation cycle with the final state. This reduces unnecessary Virtual DOM creation and DOM mutations. In React 17, batching only worked inside React event handlers.

Q:How would you optimize a React app that re-renders too often?

A: First, identify unnecessary re-renders using React DevTools Profiler. Then: (1) Use React.memo to skip re-rendering components whose props haven't changed. (2) Use useMemo/useCallback to stabilize object and function references passed as props. (3) Colocate state to the lowest component that needs it. (4) Use virtualization for long lists. (5) Avoid inline object/array creation in JSX props.

13

Practice Section

Test your understanding with these scenario-based questions. Try to reason through each one before reading the answer.

1

Slow List Rendering

A todo list with 500 items re-renders every time any single item is toggled. The entire list takes 200ms to render. Users report lag when checking items. What's happening and how do you fix it?

Answer: When one item's state changes, the parent list re-renders, which re-renders all 500 child components. Each child creates a new Virtual DOM and gets diffed — even though 499 of them are unchanged. Fix: (1) Wrap TodoItem in React.memo so unchanged items skip rendering entirely. (2) Use useCallback for the toggle handler so the function reference is stable. (3) If items have complex rendering, consider virtualization to only render visible items.

2

Wrong Keys Causing Bugs

A user reports that when they delete a row from a table, the input values in the remaining rows get shuffled. The list uses index as key. Explain why this happens.

Answer: With index keys, deleting row 0 causes row 1 to become key=0, row 2 to become key=1, etc. React sees key=0 still exists but with different text — it updates the text but keeps the existing DOM node (including the input's internal state). The input values stay attached to their index positions, not their data. Fix: use a unique ID (like row.id) as the key so React correctly identifies which row was deleted and removes only that DOM node.

3

Optimizing Re-renders

A component passes style={{ marginTop: 10 }} as a prop to a memoized child. The child still re-renders every time the parent renders. Why?

Answer: The inline object literal { marginTop: 10 } creates a new object reference on every render. React.memo does a shallow comparison of props — since the object reference is different each time (even though the values are identical), memo thinks the prop changed and re-renders. Fix: hoist the style object outside the component (const style = { marginTop: 10 }) or wrap it in useMemo. This gives a stable reference that memo can compare correctly.

4

Component Type Switch

You have a form that conditionally renders either <TextInput /> or <TextArea /> at the same position based on a toggle. Users report that switching between them clears the entered text. Why?

Answer: When the component type changes from TextInput to TextArea (or vice versa), React's first diffing heuristic kicks in: different types = different trees. React unmounts TextInput entirely (destroying its state and DOM), then mounts a fresh TextArea. The entered text is lost because it was part of the old component's state. Fix: use the same component type and pass a prop to control behavior, or use a controlled component with state lifted to the parent.

5

Key Reset Pattern

You want to completely reset a form component's internal state when the user switches between 'Create' and 'Edit' modes. How can you use reconciliation to your advantage?

Answer: Change the key prop on the form component: <Form key={mode} />. When mode changes from 'create' to 'edit', the key changes, so React treats it as a completely different element. It unmounts the old Form (clearing all state) and mounts a fresh one. This is the 'key reset pattern' — intentionally using React's reconciliation behavior to reset component state without manual cleanup.

14

Cheat Sheet (Quick Revision)

One-screen summary for quick revision before interviews.

Quick Revision Cheat Sheet

Virtual DOM: Lightweight JS object tree mirroring the real DOM. Fast to create and compare.

Reconciliation: React's process of diffing old vs new Virtual DOM and applying minimal changes to real DOM.

Diffing algorithm: O(n) heuristic: different types = different trees, keys identify stable elements.

Why not O(n³): General tree diff is O(n³). React's heuristics (type check + keys) reduce to O(n) — comparing level by level.

Keys: Tell React which elements are the same across renders. Must be unique among siblings and stable.

Index as key: Only safe for static, never-reordered lists with no state. Use unique IDs in all other cases.

Type change: Different element type → destroy old subtree entirely, build new one. All state is lost.

Props change: Same element type → keep DOM node, update only changed attributes.

Render phase: Pure computation: call components, build VDOM, diff trees. Can be paused (Fiber).

Commit phase: Apply DOM mutations synchronously. Run effects after. Cannot be interrupted.

React.memo: Skips re-render if props unchanged (shallow compare). Prevents unnecessary VDOM creation.

Fiber: React 16+ reconciler. Linked-list tree, interruptible rendering, priority-based scheduling.

Concurrent rendering: React 18+. Prepare multiple UI versions simultaneously. startTransition, useDeferredValue, Suspense.

Batching (React 18): All setState calls batched into one render — even in promises, timeouts, and native events.

Key reset pattern: Change key to force unmount/remount. Useful for resetting component state.