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.
Table of Contents
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.
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 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
| Aspect | Direct DOM | Virtual DOM (React) |
|---|---|---|
| Update strategy | Modify DOM nodes directly | Diff in memory, patch minimal changes |
| Performance | Each change triggers reflow/repaint | Batched updates, minimal DOM touches |
| Developer experience | Manual tracking of what changed | Declarative — describe UI, React handles updates |
| Complexity | Grows with app size | Abstracted away by the framework |
| Consistency | Easy to create inconsistent states | UI 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.
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.
// 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
| Property | Real DOM | Virtual DOM |
|---|---|---|
| Nature | Browser API — actual rendered elements | Plain JavaScript objects in memory |
| Speed | Slow to update (triggers rendering pipeline) | Fast to create and compare (just JS objects) |
| Memory | Heavy — each node has hundreds of properties | Lightweight — only stores type, props, children |
| Updates | Directly mutates the document | Creates new tree, diffs, then patches real DOM |
| Access | document.getElementById, querySelector | Internal to React — not directly accessible |
// 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.
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.
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.
Render Phase
React builds the new Virtual DOM tree by calling component functions/render methods. This is pure computation — no DOM mutations happen here.
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.
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.
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.
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.
// 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.
// 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.
// 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
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 }
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.
// ❌ 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.
// ✅ 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 Strategy | Example | Verdict | Why |
|---|---|---|---|
| Unique ID | key={item.id} | ✅ Best | Stable, unique — React can always identify the element |
| Unique string | key={item.email} | ✅ Good | Works if the value is unique and doesn't change |
| Array index | key={index} | ❌ Bad | Breaks when items are reordered, inserted, or deleted |
| Random value | key={Math.random()} | ❌ Worst | New key every render — React destroys and recreates every element |
// ❌ 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.
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.
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.
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.
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.
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.
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.
Run Effects
After the DOM is updated, React runs useEffect callbacks and ref assignments. Cleanup functions from the previous render run first.
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.
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
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)
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)
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.
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.
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.
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.
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.
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.
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.
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
| Scenario | Why VDOM Doesn't Help | Better 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 60fps | CSS animations, requestAnimationFrame, or refs for direct DOM |
| Unnecessary re-renders | VDOM is created and diffed even if nothing changed | React.memo, useMemo, useCallback to skip re-renders |
| Canvas/WebGL rendering | VDOM can't represent canvas pixels | Use 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).
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.
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.
// 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
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.
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.
Practice Section
Test your understanding with these scenario-based questions. Try to reason through each one before reading the answer.
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.
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.
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.
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.
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.
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.