Episode - How React Works Internally — Virtual DOM, Fiber, Reconciliation & More
Episode - How React Works Internally — Virtual DOM, Fiber, Reconciliation & More
Hey everyone! Welcome back to Namaste React!
You have been using React, writing components, using hooks — but do you know what is happening inside React when you call setState or write JSX? Today we go deep inside React's engine room!
This is one of the most important topics for cracking senior frontend interviews. Let's understand how React actually works — not just how to USE it, but HOW it does what it does!
What we will cover:
- What is the Virtual DOM? Why does it exist?
- JSX → React.createElement → React Element → Real DOM
- What is Reconciliation?
- The Diffing Algorithm (React's heuristics)
- What is React Fiber? Why was it needed?
- Render Phase vs Commit Phase
- How useState works internally (Hooks linked list)
- How useEffect is scheduled
- Batching — what it is and how it works
- Concurrent Mode — interruptible rendering
- Interview Questions
1. What is the Virtual DOM? Why does it exist?
Before we understand the Virtual DOM, let's understand the problem it solves.
THE PROBLEM — Direct DOM Manipulation is EXPENSIVE:
====================================================
When you do: document.getElementById("title").textContent = "New Title"
The browser has to:
1. Parse and find the element in the DOM tree
2. Check styles that might be affected
3. Recalculate layout (reflow) — what size is this element now?
4. Repaint the pixels on screen
For a LARGE app with 500 components:
If 10 things change → 10 separate DOM updates → 10 reflows + 10 repaints
→ SLOW, janky, bad user experience!
DOM operations are the most expensive operations in the browser!
┌─────────────────────────────────────────────────────────────┐ │ THE VIRTUAL DOM SOLUTION │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Virtual DOM = A lightweight JavaScript COPY of the DOM! │ │ │ │ │ │ REAL DOM (in the browser): │ │ - Actual HTML elements │ │ - Controlled by the browser │ │ - Expensive to read and write │ │ - Causes reflows and repaints │ │ │ │ │ │ VIRTUAL DOM (in JavaScript memory): │ │ - Just plain JavaScript OBJECTS │ │ - Controlled by React │ │ - Extremely cheap to read and write │ │ - No reflows, no repaints │ │ │ └─────────────────────────────────────────────────────────────┘
How Virtual DOM Works — The 3-Step Process: ============================================ Step 1: STATE CHANGES User clicks a button → state changes React does NOT touch the real DOM yet! Step 2: DIFFING React creates a NEW virtual DOM tree Compares it with the OLD virtual DOM tree Finds EXACTLY what changed (minimal diff) Step 3: BATCH UPDATE React updates ONLY the changed parts of the real DOM One efficient batch update instead of many small updates! ┌──────────────────────────────────────────────────────────────┐ │ │ │ State Change │ │ ↓ │ │ New Virtual DOM │ │ ↓ │ │ Compare with Old Virtual DOM ← (Diffing Algorithm) │ │ ↓ │ │ Find minimum changes │ │ ↓ │ │ Update ONLY those parts in Real DOM │ │ ↓ │ │ Browser repaints just what changed! │ │ │ └──────────────────────────────────────────────────────────────┘
Important: The Virtual DOM is NOT faster than the real DOM for a single operation. What makes it fast is batching — grouping many changes and applying them all at once to the real DOM!
2. JSX → React.createElement → React Element → Real DOM
Let's trace the FULL journey from your JSX code to what appears on screen!
THE COMPLETE PIPELINE:
=======================
Your JSX Code
↓ (Babel transforms JSX)
React.createElement() calls
↓ (React executes them)
React Element (Plain JS Object = Virtual DOM Node)
↓ (ReactDOM.render / root.render)
Fiber Nodes (React's internal representation)
↓ (Reconciliation + Diffing)
Real DOM updates
↓ (Browser)
Pixels on Screen!
// STEP 1: You write JSX
function App() {
return (
<div className="app">
<h1>Hello World</h1>
<p>Welcome to React!</p>
</div>
);
}
// STEP 2: Babel transforms it to React.createElement()
function App() {
return React.createElement(
"div",
{ className: "app" },
React.createElement("h1", null, "Hello World"),
React.createElement("p", null, "Welcome to React!")
);
}
// STEP 3: React.createElement returns a React ELEMENT (plain JS object)
{
$$typeof: Symbol(react.element), // Proof it's a React element
type: "div", // What kind of element
key: null, // For reconciliation (lists)
ref: null, // For DOM access
props: {
className: "app",
children: [
{
$$typeof: Symbol(react.element),
type: "h1",
props: { children: "Hello World" },
key: null,
ref: null
},
{
$$typeof: Symbol(react.element),
type: "p",
props: { children: "Welcome to React!" },
key: null,
ref: null
}
]
}
}
// This object IS the Virtual DOM!
// It's just a plain JS object describing what SHOULD be on screen.
// It is IMMUTABLE — React never mutates it.
// STEP 4: React converts React Elements to Fiber Nodes (internal) // STEP 5: ReactDOM commits Fiber tree to real DOM // Result in the browser: // <div class="app"> // <h1>Hello World</h1> // <p>Welcome to React!</p> // </div>
React Element vs DOM Element: ============================== React Element: DOM Element: ───────────── ──────────── Plain JS Object Actual HTML node Created by createElement() Created by browser Lightweight Heavy No methods Has hundreds of methods Immutable Mutable Garbage collected Lives in browser memory Cheap to create Expensive to create
3. What is Reconciliation?
Reconciliation is the process React uses to figure out what changed between two renders and how to update the DOM as efficiently as possible.
┌─────────────────────────────────────────────────────────────┐ │ RECONCILIATION │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Before (Old Tree): After (New Tree): │ │ ════════════════ ═══════════════ │ │ │ │ <App> <App> │ │ / \ / \ │ │ <Header> <Main> <Header> <Main> │ │ | | │ │ <List> <List> │ │ / | \ / | \ │ │ <A> <B> <C> <A> <B> <D> │ │ ↑ │ │ <C> → <D> │ │ CHANGED! │ │ │ │ React compares OLD tree with NEW tree │ │ Finds that only <C> changed to <D> │ │ Updates ONLY that node in the real DOM! │ │ │ └─────────────────────────────────────────────────────────────┘
The Naive Approach (What React does NOT do): ============================================= Comparing two trees naively = O(n³) complexity! If you have 1000 nodes: 1000³ = 1,000,000,000 comparisons! 🐌 Way too slow for a real app! React's Approach — Two Heuristics: ==================================== React makes TWO assumptions to reduce complexity to O(n): 1. Elements of DIFFERENT TYPES produce different trees → Don't try to compare them — just replace! 2. The KEY prop signals which elements are stable → Use keys to match elements across renders
4. The Diffing Algorithm — React's Heuristics
The Diffing Algorithm is how React compares old vs new virtual DOM and decides what to update. Let's go through each rule:
RULE 1: Different Element Type → Destroy and Rebuild
======================================================
// Old Tree:
<div>
<Counter />
</div>
// New Tree (div changed to section):
<section>
<Counter />
</section>
React sees: "div" !== "section"
Action: DESTROY the div and everything inside it (including Counter)!
CREATE a fresh section with a fresh Counter!
Result: Counter's state is LOST! (it was destroyed and recreated)
WHY: React assumes that if the element type changed,
the entire subtree is probably different anyway.
Comparing them would be wasteful.
RULE 2: Same Element Type → Update Attributes, Keep Children
=============================================================
// Old Tree:
<div className="before" style={{color: 'red'}}>
<p>Hello</p>
</div>
// New Tree:
<div className="after" style={{color: 'blue'}}>
<p>Hello</p>
</div>
React sees: Both are "div" type
Action: Update ONLY the changed attributes!
→ Change className from "before" to "after"
→ Change color from 'red' to 'blue'
→ Keep the child <p>Hello</p> intact!
The DOM node is REUSED! Just attributes are patched.
No destruction, no recreation!
RULE 3: Same Component Type → Update Props, Preserve State
===========================================================
// Old Tree:
<Counter count={5} />
// New Tree:
<Counter count={10} />
React sees: Both are "Counter" component type
Action: Update the instance's props (count: 5 → 10)
Keep the same component INSTANCE!
Trigger re-render with new props.
STATE is PRESERVED! (because same instance)
This is why same-type component updates don't lose state!
RULE 4: Lists — The KEY Problem
=================================
// Old List:
<ul>
<li>Apple</li> ← index 0
<li>Banana</li> ← index 1
<li>Cherry</li> ← index 2
</ul>
// New List (item added at START):
<ul>
<li>Mango</li> ← index 0 (NEW!)
<li>Apple</li> ← index 1 (was 0)
<li>Banana</li> ← index 2 (was 1)
<li>Cherry</li> ← index 3 (was 2)
</ul>
Without KEY:
React compares by POSITION (index)!
index 0: "Apple" vs "Mango" → different! Update DOM!
index 1: "Banana" vs "Apple" → different! Update DOM!
index 2: "Cherry" vs "Banana" → different! Update DOM!
index 3: nothing vs "Cherry" → Create new DOM!
React updated ALL 4 items. But only 1 truly changed!
INEFFICIENT! ❌
With KEY:
React compares by KEY value!
Old: key="apple" Apple, key="banana" Banana, key="cherry" Cherry
New: key="mango" Mango, key="apple" Apple, key="banana" Banana, key="cherry" Cherry
React sees:
- key="apple" → Same! Just MOVED to index 1. (move DOM node)
- key="banana" → Same! Just MOVED to index 2. (move DOM node)
- key="cherry" → Same! Just MOVED to index 3. (move DOM node)
- key="mango" → NEW! Create ONE new DOM node.
Only 1 DOM creation instead of 4 DOM mutations! EFFICIENT! ✅
WHY YOU SHOULD NOT USE INDEX AS KEY:
======================================
// ❌ BAD — using index as key
items.map((item, index) => <li key={index}>{item.name}</li>)
If items are sorted, filtered, or reordered:
→ Keys don't help because they just become 0, 1, 2, 3...
→ Same performance problem as no key!
// ✅ GOOD — use stable, unique ID
items.map(item => <li key={item.id}>{item.name}</li>)
Using index as key is only safe when:
1. The list never reorders
2. Items are never added to the beginning or middle
3. List items have no state
5. What is React Fiber? Why was it needed?
React Fiber is the complete reimplementation of React's core algorithm, introduced in React 16. Understanding why it was needed is as important as understanding what it is!
THE PROBLEM WITH REACT BEFORE FIBER (React 15 and earlier):
=============================================================
Old React used a RECURSIVE algorithm called the "Stack Reconciler":
function renderComponent(component) {
// Recursively renders all children
renderComponent(child1);
renderComponent(grandchild1);
renderComponent(greatGrandchild1);
// Goes all the way down...
}
The problem: ONCE STARTED, IT CANNOT STOP!
If your component tree is large:
→ Stack reconciler starts rendering
→ JavaScript is single-threaded
→ Main thread is BLOCKED
→ No user interaction (clicks, typing) can happen
→ No animations can run
→ Page feels frozen and unresponsive!
Example:
A complex React app update takes 300ms
The browser can't paint frames for 300ms
User sees frozen UI every time state changes!
This is called "jank" — 🐌
┌─────────────────────────────────────────────────────────────┐ │ STACK RECONCILER (Pre-Fiber) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Timeline: │ │ │ │ [React rendering... 300ms ...........] [User click] │ │ ↑ │ │ User had to wait! │ │ │ │ React blocked the main thread for 300ms! │ │ User couldn't interact with the page! │ │ │ └─────────────────────────────────────────────────────────────┘
THE SOLUTION — REACT FIBER: ============================ Fiber reimagines reconciliation as a LINKED LIST instead of a call stack. Each piece of work is a "Fiber" node. Fiber can be PAUSED, RESUMED, ABORTED, or REUSED! React works in small chunks and yields control back to the browser! ┌─────────────────────────────────────────────────────────────┐ │ FIBER RECONCILER │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Timeline: │ │ │ │ [React chunk1][browser][React chunk2][browser][React chunk3]│ │ ↑ ↑ │ │ Browser gets Browser gets │ │ to paint! to handle click! │ │ │ │ React works in slices, yielding to browser between each! │ │ No more frozen UI! │ │ │ └─────────────────────────────────────────────────────────────┘
WHAT IS A FIBER NODE?
=======================
A Fiber is a JavaScript object representing ONE unit of work.
Every React element has a corresponding Fiber node.
// Simplified Fiber Node structure:
{
// Identity
type: "div" | MyComponent | null,
key: "some-key" | null,
// Linked list pointers
child: Fiber | null, // First child
sibling: Fiber | null, // Next sibling
return: Fiber | null, // Parent
// Work
pendingProps: {}, // Props for this render
memoizedProps: {}, // Props from last render
memoizedState: {}, // State from last render (hooks linked list!)
// Effects
effectTag: "PLACEMENT" | "UPDATE" | "DELETION",
effectList: Fiber[], // List of changes to commit to DOM
// Scheduling
expirationTime: number, // When this work must be done by
lanes: Lanes, // Priority (React 18+ uses lanes)
// Alternate (double buffering)
alternate: Fiber | null, // Points to the other tree's fiber
}
FIBER'S TREE STRUCTURE — Linked List, Not Recursion:
======================================================
<App> ← Fiber Node
/ \
<Header> <Main> ← Fiber Nodes
|
<List> ← Fiber Node
/ | \
<A> <B> <C> ← Fiber Nodes
Each Fiber node has 3 pointers:
→ child: points to FIRST child
→ sibling: points to NEXT sibling
→ return: points to PARENT
React traverses this like:
1. Go to App.child → Header
2. Header has no children → Header.sibling → Main
3. Main.child → List
4. List.child → A
5. A.sibling → B
6. B.sibling → C
7. C has no sibling → C.return → List
8. List.return → Main
9. etc.
At ANY point, React can pause this traversal!
It saves where it stopped in the Fiber node.
Later it can RESUME from that exact point!
6. Render Phase vs Commit Phase
React's work is split into two distinct phases. Understanding the difference is CRITICAL!
┌─────────────────────────────────────────────────────────────┐ │ REACT'S TWO PHASES │ └─────────────────────────────────────────────────────────────┘ PHASE 1 — RENDER PHASE (also called Reconciliation Phase): =========================================================== What happens: → React calls your component functions → React.createElement() creates React elements → Fiber nodes are created / updated → Diffing algorithm runs (compare old vs new) → React builds a list of changes needed (effects list) Key characteristics: ✅ Can be PAUSED and RESUMED (thanks to Fiber!) ✅ Can be ABORTED if higher-priority work comes in ✅ Runs asynchronously (in Concurrent Mode) ❌ NO real DOM changes yet! ❌ NO side effects should run here React 18: Render phase can be interrupted! React may call your component function MULTIPLE TIMES in one render! That's why you should NOT put side effects directly in the component body. PHASE 2 — COMMIT PHASE: ======================== What happens: → React takes the effects list from render phase → Applies all DOM changes at once (synchronously!) → Runs lifecycle methods / useEffect cleanup + setup → DOM is updated, browser can paint Key characteristics: ❌ CANNOT be paused or interrupted ❌ Runs synchronously and blocks the main thread ✅ After this, the browser paints the new pixels
COMMIT PHASE — 3 Sub-Phases: ============================== ┌────────────────────────────────────────────────────────┐ │ COMMIT PHASE │ │ │ │ Sub-phase 1: "before mutation" │ │ → getSnapshotBeforeUpdate() is called │ │ → DOM not yet mutated │ │ │ │ Sub-phase 2: "mutation" │ │ → DOM is actually updated! (insertions, deletions) │ │ → componentWillUnmount() for removed components │ │ → refs are detached from old nodes │ │ │ │ Sub-phase 3: "layout" │ │ → DOM is updated, layout effects run synchronously │ │ → componentDidMount() / componentDidUpdate() called │ │ → useLayoutEffect callbacks run │ │ → refs are attached to new DOM nodes │ │ │ │ AFTER COMMIT (async, scheduled): │ │ → Browser paints the screen │ │ → useEffect callbacks run │ │ │ └────────────────────────────────────────────────────────┘
DOUBLE BUFFERING — The "Work in Progress" Tree: ================================================ React ALWAYS keeps TWO fiber trees in memory! Current Tree: Work-in-Progress Tree: ───────────── ────────────────────── What's on screen Being built for next render right now React builds the Work-in-Progress tree silently. Once done → swap! WIP becomes Current. Old Current is RECYCLED as the next WIP tree. This is called DOUBLE BUFFERING! Benefits: ✅ User always sees a COMPLETE, consistent UI (Current tree) ✅ WIP tree can be built/modified without affecting what user sees ✅ If something goes wrong in WIP tree → just throw it away! ✅ No partial renders visible to the user! // Each Fiber node's .alternate property points to the other tree's fiber! currentFiber.alternate === wipFiber wipFiber.alternate === currentFiber
7. How useState Works Internally — The Hooks Linked List
This will BLOW YOUR MIND! Hooks are not magic — they're a carefully designed linked list!
THE BIG QUESTION: Where does React store state?
=================================================
When you call useState in a function component:
const [count, setCount] = useState(0);
React stores the state in the component's FIBER NODE!
Specifically, in the fiber's "memoizedState" property.
But what if you have MULTIPLE hooks?
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");
const [active, setActive] = useState(false);
React stores them as a LINKED LIST on the fiber node!
// When React mounts this component:
function Counter() {
const [count, setCount] = useState(0); // Hook 1
const [name, setName] = useState("Al"); // Hook 2
const [on, setOn] = useState(false); // Hook 3
// ...
}
// React builds this structure on the Fiber node:
fiber.memoizedState = {
// Hook 1 — useState(0)
memoizedState: 0, // Current state value
queue: { ... }, // Update queue
next: {
// Hook 2 — useState("Al")
memoizedState: "Al", // Current state value
queue: { ... }, // Update queue
next: {
// Hook 3 — useState(false)
memoizedState: false,
queue: { ... },
next: null // End of list
}
}
}
// It's a LINKED LIST! Each hook points to the NEXT hook!
THIS IS WHY HOOKS MUST BE CALLED IN THE SAME ORDER!
=====================================================
// React reads hooks by POSITION in the list!
// First call = hook #1 in list
// Second call = hook #2 in list
// etc.
// FIRST RENDER:
// useState(0) → linked list position 1 → value: 0
// useState("Al") → linked list position 2 → value: "Al"
// useState(false) → linked list position 3 → value: false
// SECOND RENDER:
// useState(0) → reads position 1 → gets: 0 ✅
// useState("Al") → reads position 2 → gets: "Al" ✅
// useState(false) → reads position 3 → gets: false ✅
// IF YOU ADD A CONDITIONAL HOOK:
function Counter({ show }) {
const [count, setCount] = useState(0); // position 1
if (show) {
const [extra, setExtra] = useState(99); // ❌ position 2 (sometimes!)
}
const [name, setName] = useState("Al"); // position 2 OR 3 — CONFUSED!
}
// When 'show' changes from true to false:
// position 1 → count: 0 ✅
// position 2 → name reads "extra"'s value 99 ❌ WRONG!
// This is WHY React has the Rule: "Only call Hooks at the top level"
// No conditions, no loops!
HOW setState TRIGGERS A RE-RENDER:
====================================
const [count, setCount] = useState(0);
// When you call: setCount(5)
// What React does internally:
// 1. Create an "Update" object:
const update = {
action: 5, // new state value (or reducer function)
next: null // next update in queue (multiple setStates)
};
// 2. Add to the hook's update queue:
hook.queue.pending = update;
// 3. Schedule a re-render for this component:
scheduleUpdateOnFiber(fiber, lane);
// 4. React reconciler picks it up, runs the component function again
// 5. useState reads the queue, applies updates, returns new value
// 6. React diffs old vs new output, commits changes to DOM
// BATCHING: Multiple setStates in the same event handler
// are batched into ONE re-render!
function handleClick() {
setCount(1); // Queued, no immediate re-render
setName("Bob"); // Queued, no immediate re-render
setActive(true); // Queued, no immediate re-render
// ONE re-render happens after event handler completes!
}
8. How useEffect is Scheduled
useEffect is NOT called immediately after render!
==================================================
// React's schedule for useEffect:
// 1. Component renders (render phase)
// 2. React commits DOM changes (commit phase — synchronous)
// 3. Browser PAINTS the screen (user sees updated UI)
// 4. useEffect runs (after paint — asynchronous!)
// This is why useEffect is safe for API calls:
// The user already sees the UI before the fetch starts!
useEffect(() => {
console.log("I run AFTER the browser painted!");
// API calls go here — they don't block the UI
fetchData().then(data => setData(data));
}, []);
HOW REACT SCHEDULES useEffect INTERNALLY: ========================================== After the commit phase, React doesn't call useEffect immediately. It schedules them using: React uses a custom scheduler that uses: 1. MessageChannel (preferred — fires after paint) 2. setTimeout(fn, 0) (fallback) // Why MessageChannel instead of setTimeout? // - setTimeout has minimum delay (4ms minimum in browsers) // - MessageChannel fires as a macrotask — after paint but no delay! // React's internal scheduling: const channel = new MessageChannel(); channel.port1.onmessage = runEffects; // runs useEffect callbacks channel.port2.postMessage(null); // triggers after current paint!
CLEANUP AND RE-EXECUTION:
==========================
useEffect(() => {
// ① Setup: runs after first render (mount)
console.log("Effect setup");
const subscription = subscribe(props.id);
return () => {
// ② Cleanup: runs BEFORE next effect OR on unmount
console.log("Effect cleanup");
unsubscribe(subscription);
};
}, [props.id]); // Dependency array
Timeline:
─────────
Mount:
→ Component renders
→ DOM committed
→ Browser paints
→ ① Setup runs (subscribe to props.id)
props.id changes (re-render):
→ Component renders with new props.id
→ DOM committed
→ Browser paints
→ ② Cleanup runs (unsubscribe OLD id)
→ ① Setup runs (subscribe NEW id)
Unmount:
→ ② Cleanup runs (unsubscribe)
→ Component removed from DOM
React ALWAYS runs cleanup before re-running the effect!
This prevents stale subscriptions, memory leaks, etc.
9. Batching — What it is and How it Works
WHAT IS BATCHING?
==================
Batching = grouping multiple state updates into ONE re-render!
Without batching:
setCount(1) → re-render #1
setName("X") → re-render #2 (wasted!)
setActive(true) → re-render #3 (wasted!)
With batching:
setCount(1) ↘
setName("X") → ONE re-render at the end!
setActive(true) ↗
HOW IT WORKS IN REACT 17 AND BEFORE:
======================================
// React 17 batches ONLY inside event handlers!
function handleClick() {
setCount(1); // Batched!
setName("Bob"); // Batched!
// → ONE re-render after handler completes ✅
}
// BUT NOT in async code!
async function handleClick() {
const data = await fetchData();
setCount(data.count); // NOT batched! → re-render #1
setName(data.name); // NOT batched! → re-render #2
// → TWO separate re-renders ❌
}
HOW IT WORKS IN REACT 18:
===========================
// React 18 introduces AUTOMATIC BATCHING!
// Batches ALL state updates — even in async code, promises, setTimeout!
// Inside event handler:
function handleClick() {
setCount(1); // Batched! ✅
setName("Bob"); // Batched! ✅
// → ONE re-render
}
// In async code (REACT 18!):
async function handleClick() {
const data = await fetchData();
setCount(data.count); // Batched! ✅ (React 18)
setName(data.name); // Batched! ✅ (React 18)
// → ONE re-render!
}
// In setTimeout (REACT 18!):
setTimeout(() => {
setCount(1); // Batched! ✅ (React 18)
setName("X"); // Batched! ✅ (React 18)
// → ONE re-render!
}, 1000);
// Opting OUT of batching in React 18 (rarely needed):
import { flushSync } from "react-dom";
function handleClick() {
flushSync(() => {
setCount(1); // Forces immediate re-render!
});
// DOM is updated here!
flushSync(() => {
setName("Bob"); // Forces another immediate re-render!
});
// DOM is updated again here!
}
// Only use flushSync if you need to read the DOM
// between state updates. (Very rare case!)
10. Concurrent Mode — Interruptible Rendering
WHAT IS CONCURRENT MODE? ========================= Concurrent Mode is React's ability to work on MULTIPLE renders simultaneously and to INTERRUPT lower-priority work for higher-priority work! Think of it like a restaurant: Old React (Legacy Mode) = Single waiter. Takes order, makes it, delivers. Nobody else gets served until first order is done! React Concurrent Mode = Multiple waiters with PRIORITIES. If a VIP walks in, they get served immediately. The waiter pauses the current order, serves VIP, then comes back!
┌─────────────────────────────────────────────────────────────┐ │ CONCURRENT MODE PRIORITIES │ ├─────────────────────────────────────────────────────────────┤ │ │ │ React 18 uses "Lanes" — a bitmask priority system │ │ │ │ Priority Level Lane Use Case │ │ ────────────────────────────────────────────────── │ │ Immediate SyncLane Discrete events │ │ (Highest) (clicking, typing) (user input!) │ │ │ │ Normal DefaultLane useState, data │ │ │ │ Transition TransitionLane useTransition, │ │ (Lower) slow renders │ │ │ │ Deferred OffscreenLane Background work, │ │ (Lowest) Suspense (hidden) │ │ │ └─────────────────────────────────────────────────────────────┘
useTransition — Marking Low-Priority Updates:
=============================================
// Without useTransition:
function SearchBox() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
function handleChange(e) {
setQuery(e.target.value); // Fast: update input
setResults(searchItems(query)); // Slow: search 10,000 items
// Both run at same priority → input feels laggy during search!
}
return <input onChange={handleChange} value={query} />;
}
// With useTransition:
import { useTransition, useState } from "react";
function SearchBox() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // HIGH priority → updates immediately
startTransition(() => {
// LOW priority → can be interrupted by user input!
setResults(searchItems(e.target.value));
});
}
return (
<div>
<input onChange={handleChange} value={query} />
{isPending && <p>Searching...</p>}
<SearchResults results={results} />
</div>
);
}
// Now: if user types fast, React can INTERRUPT the search render
// and immediately render the new input value!
// User feels snappy, even with slow search results!
useDeferredValue — Deferring a Value:
=======================================
// useDeferredValue is like debouncing in React's render phase
// The deferred value "lags behind" the current value
function SearchBox() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
// deferredQuery is always slightly behind query!
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
{/* SlowList uses deferredQuery — won't block typing! */}
<SlowList query={deferredQuery} />
</div>
);
}
// useTransition vs useDeferredValue:
// useTransition — you control what code is low priority
// useDeferredValue — you control what VALUE is deferred
// Both achieve the same goal, different API
Suspense and Concurrent Mode:
===============================
// Suspense lets components "wait" for something before rendering
// In Concurrent Mode, React can render other things while waiting!
import { Suspense, lazy } from "react";
const LazyComponent = lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
{/* React will render the fallback while LazyComponent loads */}
{/* Other parts of the page remain interactive! */}
<LazyComponent />
</Suspense>
);
}
// With Concurrent Mode:
// → React starts rendering LazyComponent
// → Sees it's suspended (import hasn't loaded yet)
// → Shows fallback UI
// → Continues rendering OTHER parts of the tree!
// → When LazyComponent is ready → renders it
// → No blocking! User can still interact with the page!
The Full Picture — Everything Together
┌─────────────────────────────────────────────────────────────────┐
│ REACT COMPLETE INTERNAL FLOW │
└─────────────────────────────────────────────────────────────────┘
1. YOU WRITE JSX
<Counter count={5} />
2. BABEL TRANSFORMS → React.createElement()
React.createElement(Counter, { count: 5 })
3. REACT CREATES A REACT ELEMENT (Virtual DOM node = JS object)
{ type: Counter, props: { count: 5 }, ... }
4. REACT CREATES/UPDATES A FIBER NODE
fiber.pendingProps = { count: 5 }
5. RENDER PHASE — React builds Work-in-Progress tree
↳ Calls your Counter function with new props
↳ Diffs WIP tree vs Current tree
↳ Builds list of changes (effects)
↳ CAN BE INTERRUPTED by higher-priority work!
6. COMMIT PHASE — React applies changes to real DOM
↳ Before mutation: getSnapshotBeforeUpdate
↳ Mutation: actual DOM inserts/updates/deletes
↳ Layout: useLayoutEffect, componentDidUpdate
↳ CANNOT be interrupted
7. BROWSER PAINTS
↳ User sees updated UI
8. PASSIVE EFFECTS
↳ useEffect cleanup runs (for previous render)
↳ useEffect setup runs (for current render)
9. STATE UPDATE (setCount called)
↳ Update object added to hook's queue
↳ Re-render scheduled with appropriate priority
↳ Go back to step 5!
Interview Questions — Quick Fire!
Q: What is the Virtual DOM?
"The Virtual DOM is a lightweight JavaScript representation of the real DOM, stored as plain objects. React uses it to batch updates — instead of directly touching the expensive real DOM on every state change, React first updates the Virtual DOM, diffs it against the previous version, and then applies only the minimum set of changes to the real DOM in one batch."
Q: What is the Diffing Algorithm?
"React's diffing algorithm compares two Virtual DOM trees with O(n) complexity using two heuristics: First, if elements have different types, React destroys the old tree and builds a fresh one. Second, React uses keys to identify which list items are stable across renders, avoiding unnecessary DOM mutations when lists are reordered."
Q: Why should we not use array index as a key in React?
"Using index as a key causes incorrect reconciliation when items are added, removed, or reordered. Since the key is tied to position rather than the item's identity, React can't tell that an item moved — it sees the same key at position 0 but with different content, so it updates the DOM instead of just moving it. This can also cause state bugs in components that have their own state."
Q: What is React Fiber?
"React Fiber is the complete reimplementation of React's core algorithm introduced in React 16. It replaces the old recursive Stack Reconciler with a linked-list-based Fiber structure. Each unit of work (Fiber node) can be paused, resumed, prioritized, or aborted. This enables features like concurrent rendering, time slicing, and Suspense — because React can now yield control back to the browser between rendering chunks."
Q: What is the difference between the Render phase and the Commit phase?
"The Render phase is when React builds the work-in-progress fiber tree, runs the diffing algorithm, and determines what changes are needed — but makes NO DOM changes. It can be paused and is pure. The Commit phase is when React applies those changes to the real DOM synchronously and runs lifecycle methods/effects. The Commit phase cannot be interrupted."
Q: Why can't you call Hooks inside conditions or loops?
"React stores hook state as a linked list on the fiber node. Each hook is identified by its position in the list — not by name. If you call hooks conditionally, the list order changes between renders, and React reads the wrong state for each hook. This leads to bugs where one hook reads another hook's state. Always calling hooks at the top level ensures the linked list is the same every render."
Q: What is Automatic Batching in React 18?
"Automatic Batching in React 18 groups all state updates into a single re-render, even inside async functions, Promises, or setTimeout — not just event handlers. Before React 18, batching only worked inside React event handlers. Now any setState calls within the same async operation are batched together, reducing unnecessary re-renders."
Q: What is useTransition?
"useTransition marks a state update as non-urgent. React will render high-priority updates (like user input) first and can interrupt the transition update if new high-priority work arrives. It returns isPending (boolean — is the transition in progress?) and startTransition (function — wraps the low-priority state update). It's used for slow renders that shouldn't block the user interface."
Q: What is Double Buffering in React Fiber?
"React always maintains two fiber trees: the Current tree (what's on screen) and the Work-in-Progress tree (being built for the next render). React builds the WIP tree silently. Once complete, it swaps them — WIP becomes Current. The old Current becomes the next WIP. This ensures the user always sees a complete UI, never a half-rendered state. Each fiber node's .alternate property points to the corresponding node in the other tree."
Q: What is the difference between useEffect and useLayoutEffect timing?
"useLayoutEffect runs synchronously after all DOM mutations but BEFORE the browser paints. useEffect runs asynchronously AFTER the browser has painted the screen. Use useLayoutEffect when you need to read DOM measurements and prevent visual flickering. Use useEffect (default choice) for everything else — API calls, subscriptions, timers."
Q: What is Concurrent Mode?
"Concurrent Mode is React's ability to prepare multiple versions of the UI simultaneously and to interrupt low-priority renders when high-priority work arrives. It uses a priority system (Lanes) — user interactions get highest priority, data fetching transitions get lower priority. It enables features like useTransition, useDeferredValue, automatic batching, and improved Suspense. React 18 enables Concurrent Mode when you use createRoot()."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| Virtual DOM | Lightweight JS object copy of the DOM. React batches changes and applies minimum DOM updates. |
| React.createElement | Returns a plain JS object (React element = Virtual DOM node). NOT a real DOM element. |
| Reconciliation | Process of diffing old vs new virtual DOM to find minimum DOM changes. O(n) with 2 heuristics. |
| Diffing — Types | Different type → destroy + recreate. Same type → update attributes. Component same type → update props, preserve state. |
| Keys | Help React identify list items across renders. Use stable unique IDs. Never use array index for mutable lists. |
| React Fiber | Linked-list based reconciler (React 16+). Each unit of work is a Fiber node. Can pause, resume, abort. |
| Render Phase | Builds WIP fiber tree, diffs, finds changes. Pure, can be interrupted. NO real DOM changes. |
| Commit Phase | Applies DOM changes synchronously. Runs effects. Cannot be interrupted. 3 sub-phases. |
| Double Buffering | Current tree (on screen) + WIP tree (being built). Swap when done. User always sees complete UI. |
| Hooks Linked List | Hook state stored as linked list on fiber. Position-based, not name-based. WHY hooks can't be conditional. |
| Batching (React 18) | All setState calls batched into 1 re-render — even in async, Promises, setTimeout. Use flushSync to opt out. |
| Concurrent Mode | Interruptible rendering with priorities (Lanes). High priority (input) interrupts low priority (transitions). |
| useTransition | Marks a state update as low-priority transition. Can be interrupted. Returns [isPending, startTransition]. |
| useDeferredValue | Creates a deferred version of a value that "lags behind". Good for deferring expensive renders. |
What's Next?
Now that you understand React's internals, the next logical topics to explore are:
- React Server Components — server-side rendering with React, zero client JS for server components
- Suspense for Data Fetching — letting components "suspend" while waiting for data
- Error Boundaries — catching render errors in the component tree
- React Profiler — measuring performance, finding bottlenecks using what you now understand about renders
- Custom Hooks — building your own hooks using the linked list model you now understand
Understanding React's internals gives you a superpower: now when you debug a performance issue, a state bug, or an unexpected re-render, you know EXACTLY where to look!
Keep coding, keep learning! See you in the next one!
Post a Comment