Skip to main content

React Internals - Reconciliation

React reconciliation algorithm and diffing


Question 2: Reconciliation and Virtual DOM​

Difficulty: πŸ”΄ Hard Frequency: ⭐⭐⭐⭐ Time: 12 minutes Companies: Meta, Google, Amazon

Question​

How does React's reconciliation algorithm work? What is the Virtual DOM and why is it important?

Answer​

Reconciliation - Process of determining what changed in the UI and how to efficiently update the real DOM.

Virtual DOM - Lightweight JavaScript representation of the actual DOM that React uses for efficient diffing.

Key Points:

  1. Diffing algorithm - Compares old and new virtual DOM trees
  2. Keys - Help React identify which items changed, moved, or deleted
  3. Component types - Different types always create new trees
  4. Batching - Groups multiple updates for efficiency
  5. Heuristics - React uses assumptions to make diffing O(n) instead of O(nΒ³)

Code Example​

// 1. VIRTUAL DOM CONCEPT
// Real DOM
<div id="app">
<h1>Hello</h1>
<button>Click</button>
</div>

// Virtual DOM (JavaScript object)
const vdom = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Hello'] },
{ type: 'button', props: {}, children: ['Click'] }
]
};

// 2. RECONCILIATION ALGORITHM

// ❌ WITHOUT KEYS - React can't track items efficiently
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li>{todo.text}</li> // No key!
))}
</ul>
);
}

// When a todo is added at the start:
// OLD: [Buy milk, Walk dog]
// NEW: [Write code, Buy milk, Walk dog]
// React re-renders ALL items (inefficient)

// βœ… WITH KEYS - React tracks items efficiently
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li> // Unique key
))}
</ul>
);
}

// With keys:
// React knows "Write code" is new, others stayed same
// Only renders the new item

// 3. ELEMENT TYPE CHANGES
function App() {
const [isButton, setIsButton] = useState(true);

return isButton ? (
<button onClick={() => setIsButton(false)}>
I'm a button
</button>
) : (
<div onClick={() => setIsButton(true)}>
I'm a div
</div>
);
}

// When type changes (button β†’ div):
// - Old button is DESTROYED (unmounted)
// - New div is CREATED (mounted fresh)
// - Component state is LOST

// 4. SAME TYPE, DIFFERENT PROPS
function App() {
const [className, setClassName] = useState('blue');

return (
<div className={className}>
<Child />
</div>
);
}

// When className changes:
// - div stays (not destroyed)
// - Only className attribute updates
// - Child component NOT re-rendered (same instance)

// 5. COMPONENT KEYS RESET STATE
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Task 1' },
{ id: 2, text: 'Task 2' }
]);

return todos.map(todo => (
// key prop forces React to create new instance if id changes
<TodoItem key={todo.id} todo={todo} />
));
}

// 6. BATCHING UPDATES
function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
// These are BATCHED into one re-render
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Result: count = 1 (not 3!)
// Because all use same initial count value
};

const handleClickCorrect = () => {
// Use functional updates for correct behavior
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
// Result: count = 3 βœ…
};

return <button onClick={handleClickCorrect}>Count: {count}</button>;
}

// 7. RECONCILIATION HEURISTICS

// Heuristic 1: Different types = new tree
// Bad: switching component types loses state
function Parent({ useNewComponent }) {
return useNewComponent ? <NewVersion /> : <OldVersion />;
// State is lost on switch!
}

// Good: Keep same type, change props
function Parent({ useNewComponent }) {
return <Component isNew={useNewComponent} />;
// State preserved!
}

// Heuristic 2: Keys identify elements across renders
function List({ items }) {
return items.map((item, index) => (
// ❌ BAD: Using index as key
<Item key={index} data={item} />
// Problems:
// - Reordering items causes incorrect renders
// - Removing items causes incorrect state association
));
}

function List({ items }) {
return items.map(item => (
// βœ… GOOD: Using stable unique ID
<Item key={item.id} data={item} />
));
}

// 8. DIFFING CHILDREN (Simplified algorithm)
function diffChildren(oldChildren, newChildren) {
// React's actual algorithm is more complex, but concept:

// 1. Build map of old children by key
const oldMap = new Map();
oldChildren.forEach((child, i) => {
const key = child.key || i;
oldMap.set(key, child);
});

// 2. Iterate new children
newChildren.forEach((newChild, i) => {
const key = newChild.key || i;
const oldChild = oldMap.get(key);

if (!oldChild) {
// New child - mount
mount(newChild);
} else if (needsUpdate(oldChild, newChild)) {
// Existing child - update
update(oldChild, newChild);
}
// else - no change, reuse existing
});

// 3. Remove old children not in new list
oldChildren.forEach(oldChild => {
if (!newChildren.includes(oldChild)) {
unmount(oldChild);
}
});
}

Common Mistakes​

  • ❌ Using index as key when list can reorder/filter
  • ❌ Using random values as keys (new key = new component instance)
  • ❌ Not understanding that type changes destroy component state
  • ❌ Forgetting that batching affects multiple setState calls
  • βœ… Use stable, unique IDs for keys
  • βœ… Keep component types consistent to preserve state
  • βœ… Use functional updates when new state depends on old state

Follow-up Questions​

  1. Why are keys important in React lists?
  2. What happens when a component's type changes?
  3. How does React achieve O(n) diffing complexity?

Resources​


<details> <summary><strong>πŸ” Deep Dive: React's Reconciliation Algorithm & Virtual DOM Architecture</strong></summary>

The O(n) Diffing Algorithm Breakthrough​

Traditional tree diffing algorithms have O(nΒ³) complexity - comparing two trees with 1000 nodes requires a billion operations. React achieves O(n) through three strategic heuristics that sacrifice theoretical completeness for practical performance:

Heuristic 1: Cross-Level Movement is Rare React never moves components between different levels of the tree. If a component moves from depth 3 to depth 5, React destroys and recreates it rather than attempting relocation. This eliminates the need to track element movement across levels, reducing algorithm complexity dramatically.

// Tree comparison simplified
function reconcile(oldNode, newNode, depth) {
// Only compare nodes at SAME level
if (oldNode.depth !== newNode.depth) {
// Different level = destroy + create (no movement tracking)
unmount(oldNode);
mount(newNode);
return;
}

// Same level = compare and update
if (oldNode.type === newNode.type) {
updateNode(oldNode, newNode);
}
}

Heuristic 2: Type Identity Determines Instance Identity When an element's type changes (e.g., <div> β†’ <span> or <ComponentA> β†’ <ComponentB>), React destroys the entire subtree and builds a new one. This eliminates expensive subtree comparison but means all child state is lost.

// React's type-based reconciliation
function shouldReuseInstance(oldElement, newElement) {
// Different types = completely new instance
if (oldElement.type !== newElement.type) {
return false; // Destroy old tree, create new
}

// Same type = update existing instance
return true; // Preserve instance, update props
}

// Real impact
function ConditionalRender({ useV2 }) {
// ❌ BAD: Switching types destroys state
return useV2 ? <NewComponent /> : <OldComponent />;
// Every toggle: full unmount β†’ mount cycle

// βœ… GOOD: Same type, different props preserves state
return <UniversalComponent version={useV2 ? 2 : 1} />;
}

Heuristic 3: Keys Provide Stable Identity Across Renders Keys allow React to track elements across renders even when positions change. This is critical for lists where reordering, insertion, or deletion is common.

// React's key-based reconciliation (simplified)
function reconcileChildren(oldChildren, newChildren) {
// 1. Build map of old children: key β†’ element
const oldKeyedChildren = new Map();
const oldUnkeyedChildren = [];

oldChildren.forEach((child, index) => {
if (child.key != null) {
oldKeyedChildren.set(child.key, child);
} else {
oldUnkeyedChildren.push({ index, child });
}
});

// 2. Process new children
newChildren.forEach((newChild, newIndex) => {
if (newChild.key != null) {
// Keyed element: Look up in map
const oldChild = oldKeyedChildren.get(newChild.key);
if (oldChild) {
// Found match: update if needed, move to new position
if (oldChild.props !== newChild.props) {
updateElement(oldChild, newChild);
}
moveElement(oldChild, newIndex);
oldKeyedChildren.delete(newChild.key);
} else {
// New key: mount fresh
mountElement(newChild, newIndex);
}
} else {
// Unkeyed element: match by position
const oldChild = oldUnkeyedChildren[newIndex];
if (oldChild && oldChild.child.type === newChild.type) {
updateElement(oldChild.child, newChild);
} else {
mountElement(newChild, newIndex);
}
}
});

// 3. Unmount remaining old keyed children (removed from list)
oldKeyedChildren.forEach(oldChild => {
unmountElement(oldChild);
});
}

Virtual DOM Implementation Details​

The Virtual DOM is not a mirror of the real DOM - it's a minimal representation optimized for diffing:

// React Fiber node (simplified structure)
function FiberNode(tag, props, key) {
// Instance
this.tag = tag; // HostComponent, FunctionComponent, etc.
this.key = key; // For reconciliation
this.elementType = null; // Function/class reference
this.type = null; // Resolved type
this.stateNode = null; // DOM node or class instance

// Tree structure
this.return = null; // Parent fiber
this.child = null; // First child
this.sibling = null; // Next sibling
this.index = 0; // Position in parent

// Effects
this.pendingProps = props; // New props
this.memoizedProps = null; // Previous props
this.memoizedState = null; // Previous state
this.updateQueue = null; // State/effect updates

// Reconciliation
this.alternate = null; // Previous version (double buffering)
this.flags = NoFlags; // Update/deletion/placement flags
}

Double Buffering Strategy: React maintains two fiber trees - current (on screen) and workInProgress (being built). When rendering completes, they swap:

// React's double buffering
let currentRoot = null; // Currently rendered tree
let workInProgressRoot = null; // Tree being built

function commitRoot(finishedWork) {
// Swap trees atomically
currentRoot = finishedWork;
finishedWork.alternate = null;

// Apply DOM updates
commitMutationEffects(finishedWork);
commitLayoutEffects(finishedWork);
}

function renderWithConcurrent(element) {
// Create work-in-progress tree
workInProgressRoot = createWorkInProgress(currentRoot);

// Build new tree (can be interrupted)
let nextUnitOfWork = workInProgressRoot;
while (nextUnitOfWork) {
// Check if higher priority work arrived
if (shouldYield()) {
break; // Pause rendering
}
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

// Commit completed work
if (!nextUnitOfWork) {
commitRoot(workInProgressRoot);
}
}

Batching and Update Priority​

React batches updates to minimize renders, but React 18's automatic batching extends this to all contexts:

// React 18 batching mechanism (simplified)
const updateQueue = [];
let isBatchingUpdates = false;

function scheduleUpdate(fiber, update) {
updateQueue.push({ fiber, update });

if (!isBatchingUpdates) {
isBatchingUpdates = true;

// Use microtask to batch all synchronous updates
queueMicrotask(() => {
flushUpdateQueue();
isBatchingUpdates = false;
});
}
}

function flushUpdateQueue() {
// Process all queued updates in one render cycle
const updates = updateQueue.splice(0);

// Group by priority
const syncUpdates = updates.filter(u => u.update.lane === SyncLane);
const transitionUpdates = updates.filter(u => u.update.lane === TransitionLane);

// Process high priority first
syncUpdates.forEach(({ fiber, update }) => {
applyUpdate(fiber, update);
});

render(rootFiber); // Single render for all updates
}

Component Update Optimization​

React skips rendering subtrees when parent props haven't changed:

// Bailout optimization
function beginWork(current, workInProgress) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

// Bailout conditions
if (oldProps === newProps && !hasContextChanged() && !hasForceUpdate()) {
// Props unchanged, context unchanged, no forceUpdate
// SKIP rendering this subtree entirely
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}

// Props changed, need to render
return updateFunctionComponent(current, workInProgress);
}

function bailoutOnAlreadyFinishedWork(current, workInProgress) {
// Clone children from current tree without re-rendering
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}

This optimization is why React.memo, useMemo, and useCallback are valuable - they help trigger bailouts by maintaining referential equality.


<details> <summary><strong>πŸ› Real-World Scenario: Reconciliation Performance Issues</strong></summary>

Production Case Study: E-commerce Product Grid Performance Crisis​

Context: Large e-commerce platform with 500-item product grid experiencing severe performance degradation.

Symptoms:

  • Search filtering taking 2-3 seconds to update UI
  • Input field freezing during typing
  • Layout shifts causing CLS score of 0.4
  • Mobile devices completely unresponsive
  • React DevTools Profiler showing 1800ms render times

Root Cause Analysis:

// ❌ PROBLEM CODE (actual production bug)
function ProductGrid({ searchQuery }) {
const [products, setProducts] = useState([]);

useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data));
}, []);

// MISTAKE 1: Filtering in render (blocking)
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(searchQuery.toLowerCase())
);

return (
<div className="grid">
{filteredProducts.map((product, index) => (
// MISTAKE 2: Using index as key
<ProductCard key={index} product={product} />
))}
</div>
);
}

function ProductCard({ product }) {
const [isExpanded, setIsExpanded] = useState(false);

// MISTAKE 3: Every card makes separate API call
const { data: reviews } = useQuery(`/api/reviews/${product.id}`);

return (
<div>
<h3>{product.name}</h3>
<button onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Collapse' : 'Expand'}
</button>
{isExpanded && <ReviewsList reviews={reviews} />}
</div>
);
}

Performance Metrics Before Fix:

Filtering 500 products: 1800ms render time
Input delay (INP): 580ms (Poor)
CLS: 0.4 (Poor)
Total Blocking Time: 2400ms
React reconciliation: 1200ms
DOM updates: 600ms
JavaScript execution: 1800ms

Why Performance Degraded:

  1. Index-based Keys Breaking Reconciliation:
// Search: "laptop" β†’ 100 results
// Items: [Laptop A, Laptop B, Laptop C, ...]
// Keys: [0, 1, 2, ...]

// User types "gaming laptop" β†’ 30 results
// Items: [Gaming Laptop X, Gaming Laptop Y, ...]
// Keys: [0, 1, 2, ...] (SAME KEYS, DIFFERENT PRODUCTS!)

// React's reconciliation:
// - key=0: Old="Laptop A", New="Gaming Laptop X"
// - React thinks it's same item with changed props
// - Updates ALL 100+ components unnecessarily
// - Loses expanded state (user frustration)
  1. Blocking Filter Operation:
// Every keystroke triggers:
products.filter(...) // Iterates 500 items synchronously
β†’ React starts rendering 100+ filtered items
β†’ Browser main thread BLOCKED
β†’ Input freezes until render completes
  1. Cascading API Calls:
// Rendering 100 products triggers:
100 Γ— fetch('/api/reviews/...')
β†’ Network waterfall
β†’ Staggered re-renders as each completes
β†’ Layout shifts (CLS)

Solution Implementation:

// βœ… FIXED CODE with multiple optimizations

// 1. Use stable IDs for keys
// 2. Defer expensive filtering with useTransition
// 3. Virtualize list rendering
// 4. Batch API requests

import { useState, useTransition, useDeferredValue, memo } from 'react';
import { FixedSizeGrid } from 'react-window';

function ProductGrid({ searchQuery }) {
const [products, setProducts] = useState([]);
const [isPending, startTransition] = useTransition();

// FIX 1: Defer filtering (non-urgent update)
const deferredQuery = useDeferredValue(searchQuery);

// FIX 2: Memoize expensive filter operation
const filteredProducts = useMemo(() => {
if (!deferredQuery) return products;

return products.filter(product =>
product.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [products, deferredQuery]);

// FIX 3: Prefetch reviews in batch
const productIds = filteredProducts.map(p => p.id);
const { data: reviewsMap } = useQuery(
['reviews', productIds],
() => fetchReviewsBatch(productIds), // Single API call
{ staleTime: 300000 } // Cache 5 minutes
);

return (
<div>
{isPending && <div className="loading-indicator">Filtering...</div>}

{/* FIX 4: Virtual scrolling (only render visible items) */}
<FixedSizeGrid
height={800}
width={1200}
columnCount={3}
columnWidth={400}
rowCount={Math.ceil(filteredProducts.length / 3)}
rowHeight={350}
>
{({ columnIndex, rowIndex, style }) => {
const index = rowIndex * 3 + columnIndex;
const product = filteredProducts[index];

if (!product) return null;

return (
<div style={style}>
{/* FIX 5: Stable unique key */}
<ProductCard
key={product.id}
product={product}
reviews={reviewsMap?.[product.id]}
/>
</div>
);
}}
</FixedSizeGrid>
</div>
);
}

// FIX 6: Memoize component to prevent unnecessary re-renders
const ProductCard = memo(function ProductCard({ product, reviews }) {
const [isExpanded, setIsExpanded] = useState(false);

// No API call here anymore - reviews passed from parent

return (
<div>
<h3>{product.name}</h3>
<button onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Collapse' : 'Expand'}
</button>
{isExpanded && reviews && (
<ReviewsList reviews={reviews} />
)}
</div>
);
});

Performance Metrics After Fix:

Filtering 500 products: 180ms render time (10Γ— faster)
Input delay (INP): 45ms (Good)
CLS: 0.02 (Good)
Total Blocking Time: 220ms (90% reduction)
React reconciliation: 35ms (97% faster)
DOM updates: 80ms (only visible items)
API requests: 1 batch call vs 100+ individual

Key Improvements:

  • Stable keys (product.id) β†’ Efficient reconciliation
  • useTransition β†’ Non-blocking UI updates
  • Virtual scrolling β†’ Render only 12-15 visible items instead of 500
  • Batch API β†’ 1 request instead of 100+
  • React.memo β†’ Skip re-renders when props unchanged

Debugging Steps Used:

// 1. React DevTools Profiler - identified slow renders
// Flame graph showed ProductCard re-rendering 100+ times

// 2. Performance.measure API - measured reconciliation
performance.mark('reconcile-start');
// ... React renders
performance.mark('reconcile-end');
performance.measure('reconciliation', 'reconcile-start', 'reconcile-end');

// 3. React DevTools Components - inspected keys
// Saw index-based keys changing on every filter

// 4. Network tab - discovered N+1 query problem
// 100+ parallel requests to /api/reviews/*

// 5. why-did-you-render library - caught unnecessary re-renders
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
trackAllPureComponents: true,
});

Business Impact:

  • Conversion rate increased 18% (users could actually use search)
  • Mobile bounce rate decreased 25%
  • Customer support tickets about "slow site" dropped 80%
  • SEO ranking improved due to better Core Web Vitals

Lessons Learned:

  1. Never use index as key for dynamic lists - breaks reconciliation
  2. Use transitions for expensive updates - keeps UI responsive
  3. Virtual scrolling for large lists - massive performance win
  4. Batch API requests - reduces network overhead
  5. Profile before optimizing - data-driven decisions

</details>


<details> <summary><strong>βš–οΈ Trade-offs: Virtual DOM vs Direct DOM Manipulation</strong></summary>

Virtual DOM Approach (React, Vue, Preact)​

Advantages:

  1. Declarative Programming Model:
// React: Describe UI, framework handles updates
function Counter() {
const [count, setCount] = useState(0);

return <button onClick={() => setCount(count + 1)}>
Count: {count}
</button>;
// React figures out minimal DOM operations
}

// vs. Imperative DOM manipulation
function createCounter() {
const button = document.createElement('button');
let count = 0;

function updateButton() {
button.textContent = `Count: ${count}`;
}

button.addEventListener('click', () => {
count++;
updateButton(); // Manual update
});

updateButton();
return button;
}

Winner: Virtual DOM - simpler mental model, less error-prone

  1. Automatic Optimization:
// React automatically batches these updates
function handleClick() {
setCount(c => c + 1);
setName('John');
setEmail('john@example.com');
// Only ONE DOM update, not three
}

// Manual DOM requires careful batching
function handleClickManual() {
// Causes 3 reflows without explicit batching
document.getElementById('count').textContent = count + 1;
document.getElementById('name').textContent = 'John';
document.getElementById('email').textContent = 'john@example.com';
}

Winner: Virtual DOM - automatic batching, fewer reflows

  1. Cross-Platform Compatibility:
// Same React code works everywhere
function MyComponent() {
return <Text>Hello</Text>;
}

// React DOM: <span>Hello</span>
// React Native: <Text>Hello</Text>
// React Three Fiber: 3D text object

Winner: Virtual DOM - write once, run anywhere

  1. Time Travel Debugging:
// Virtual DOM enables state snapshots
const previousVDOM = currentVDOM;
// Render with old state
render(previousVDOM); // Instant time travel!

// Direct DOM can't rewind

Winner: Virtual DOM - better developer experience

Disadvantages:

  1. Memory Overhead:
// Virtual DOM requires dual representation
const vdom = { type: 'div', props: {}, children: [...] }; // ~1KB
const realDOM = document.createElement('div'); // ~0.5KB
// Total: 1.5KB vs 0.5KB for direct DOM

For 10,000 nodes: Virtual DOM = 15MB, Direct DOM = 5MB Winner: Direct DOM - lower memory footprint

  1. Initial Render Performance:
// React (Virtual DOM)
1. Build Virtual DOM tree (JavaScript objects)
2. Run diffing algorithm
3. Generate DOM operations
4. Apply to real DOM
// Total: ~60ms for 1000 nodes

// Direct DOM
1. Create DOM nodes directly
// Total: ~25ms for 1000 nodes

Winner: Direct DOM - faster initial render

  1. Update Overhead for Simple Changes:
// Update single text node
// React path:
setState('new value')
β†’ Re-render component (5ms)
β†’ Build new VDOM (3ms)
β†’ Diff with old VDOM (2ms)
β†’ Update DOM (1ms)
Total: 11ms

// Direct DOM path:
textNode.nodeValue = 'new value';
Total: 1ms

Winner: Direct DOM - lower overhead for trivial updates

Direct DOM Manipulation (Vanilla JS, Svelte)​

Advantages:

  1. Maximum Performance for Targeted Updates:
// Svelte (compiles to direct DOM)
<script>
let count = 0;

function increment() {
count += 1; // Compiled to: countNode.textContent = count;
}
</script>

<button on:click={increment}>{count}</button>

// Compiled output (simplified):
let countNode = text(count);
function increment() {
count += 1;
countNode.data = count; // Direct update, no diffing
}

Benchmark: Svelte updates ~30% faster for simple operations

  1. Smaller Bundle Size:
// React bundle
React core: 40KB (gzipped)
ReactDOM: 130KB (gzipped)
Total: 170KB minimum

// Svelte bundle
Svelte runtime: 0KB (compiled away)
Your code: ~20KB (gzipped)
Total: 20KB for same app

Winner: Svelte - 85% smaller bundle

  1. No Runtime Overhead:
// React needs reconciler running constantly
// Svelte: All logic compiled to direct updates at build time

// React approach
function App() {
const [items, setItems] = useState([]);
return items.map(item => <Item key={item.id} {...item} />);
}
// Runtime: Reconciler runs every render

// Svelte approach
{#each items as item (item.id)}
<Item {...item} />
{/each}
// Compiled to direct DOM operations

Disadvantages:

  1. Manual Optimization Burden:
// React: Automatic optimization
function List({ items }) {
return items.map(item => <Item key={item.id} item={item} />);
// React optimizes unchanged items automatically
}

// Vanilla JS: Manual tracking
class List {
constructor(items) {
this.items = items;
this.itemMap = new Map(); // Must track manually
}

update(newItems) {
// Manual diffing logic
const oldIds = new Set(this.items.map(i => i.id));
const newIds = new Set(newItems.map(i => i.id));

// Remove deleted items
oldIds.forEach(id => {
if (!newIds.has(id)) {
this.itemMap.get(id).remove();
this.itemMap.delete(id);
}
});

// Add new items
newItems.forEach(item => {
if (!oldIds.has(item.id)) {
const el = this.createItem(item);
this.container.appendChild(el);
this.itemMap.set(item.id, el);
}
});

this.items = newItems;
}
}

Winner: Virtual DOM - less manual bookkeeping

  1. Error-Prone for Complex UIs:
// Easy to create inconsistent state
let isLoggedIn = false;

function login() {
isLoggedIn = true;
// Forgot to update UI here - BUG!
}

function render() {
if (isLoggedIn) {
// This won't run until next explicit render call
}
}

Winner: Virtual DOM - state changes automatically trigger renders

  1. No Time Travel/Replay:
// Direct DOM changes are irreversible
element.textContent = 'new value';
// Old value lost forever

// React can replay state changes
dispatch({ type: 'SET_VALUE', value: 'new' });
// All state transitions recorded

Winner: Virtual DOM - better debugging

Decision Matrix​

Choose Virtual DOM (React) when:

  • Building complex UIs with many interactive components
  • Team prefers declarative programming
  • Need cross-platform support (web + mobile)
  • Require time-travel debugging/state replay
  • Developer experience > absolute performance

Choose Direct DOM (Svelte/Vanilla) when:

  • Building simple, performance-critical apps
  • Bundle size is critical (mobile, embedded)
  • Targeting low-end devices
  • UI changes are predictable and limited
  • Performance > developer convenience

Performance Comparison (1000-item list):

Operation | React | Svelte | Vanilla
-----------------------|--------|--------|--------
Initial render | 60ms | 40ms | 25ms
Update 1 item | 8ms | 2ms | 1ms
Update 100 items | 45ms | 30ms | 15ms
Full re-render | 85ms | 55ms | 35ms
Memory (10K items) | 15MB | 8MB | 5MB
Bundle size | 170KB | 20KB | 5KB

Hybrid Approach: Modern frameworks combine both:

  • Svelte: Compile-time optimization + reactive updates
  • Solid.js: Fine-grained reactivity without Virtual DOM
  • Preact: Smaller Virtual DOM implementation (3KB)
  • Million.js: Compiler that optimizes React to skip VDOM

Real-World Recommendation:

  • Startups/MVPs: React (ecosystem, talent, velocity)
  • Performance-critical: Svelte/Solid (speed, bundle size)
  • Enterprise: React (stability, hiring, tooling)
  • Embedded/Mobile-first: Svelte (bundle size)
  • Data dashboards: Solid.js (fine-grained reactivity)

</details>


<details> <summary><strong>πŸ’¬ Explain to Junior: Reconciliation & Virtual DOM Like You're Five</strong></summary>

The Library Book Analogy​

Imagine you're a librarian with 1,000 books on shelves. Every day, some books are borrowed, returned, or moved. Your job is to keep the shelves organized efficiently.

Direct DOM Approach (Naive Librarian):

// Someone asks: "Move Harry Potter from Shelf A to Shelf B"

// Naive approach:
1. Walk to Shelf A
2. Find Harry Potter
3. Remove it
4. Walk to Shelf B
5. Place it there

// Every request requires physical work immediately

Virtual DOM Approach (Smart Librarian):

// Instead of moving books immediately, you use a notebook:

1. Write in notebook: "Harry Potter β†’ Shelf B"
2. Write in notebook: "Lord of the Rings β†’ Shelf C"
3. Write in notebook: "1984 β†’ Return bin"

// At end of day, you compare notebook with actual shelves:
4. Look at notebook: "What changed today?"
5. Plan optimal route: "I can move all books in one trip!"
6. Execute all changes together

// This is reconciliation - batch work is more efficient

The Recipe Card Metaphor (for keys)​

Your mom asks you to make a shopping list from recipe cards:

// Recipe cards
const recipes = [
{ id: 'r1', name: 'Pizza', ingredients: ['flour', 'cheese'] },
{ id: 'r2', name: 'Pasta', ingredients: ['pasta', 'sauce'] },
{ id: 'r3', name: 'Salad', ingredients: ['lettuce', 'tomato'] }
];

// ❌ WITHOUT KEYS (using position)
// Mom says: "Add Burger at the start"
// Old list:
// Position 1: Pizza
// Position 2: Pasta
// Position 3: Salad

// New list:
// Position 1: Burger
// Position 2: Pizza
// Position 3: Pasta
// Position 4: Salad

// You think every position changed, so you:
// - Cross out entire old list
// - Rewrite everything from scratch
// (Lots of work!)

// βœ… WITH KEYS (using recipe ID)
// Old list:
// r1: Pizza
// r2: Pasta
// r3: Salad

// New list:
// r4: Burger (NEW - just add this one)
// r1: Pizza (same - keep it)
// r2: Pasta (same - keep it)
// r3: Salad (same - keep it)

// You only write "Burger" once
// (Much less work!)

The Assembly Line Explanation (for batching)​

Imagine a toy factory with robots:

// WITHOUT BATCHING (React 17 in setTimeout)
Robot gets orders:
1. "Paint this car red" β†’ Stop line, paint, restart (2 seconds)
2. "Paint this car blue" β†’ Stop line, paint, restart (2 seconds)
3. "Paint this car green" β†’ Stop line, paint, restart (2 seconds)
Total time: 6 seconds (stopping/starting 3 times)

// WITH BATCHING (React 18 everywhere)
Robot gets orders:
1. "Paint this car red"
2. "Paint this car blue"
3. "Paint this car green"
β†’ Collect all orders
β†’ Stop line ONCE
β†’ Paint all three cars
β†’ Restart line ONCE
Total time: 3 seconds (stopping/starting 1 time)

// This is why React 18 automatic batching makes apps faster

Interview Answer Template​

Q: "What is reconciliation in React?"

Template Answer:

"Reconciliation is React's process for updating the UI efficiently. Instead of recreating everything when state changes, React compares the old and new Virtual DOM to find minimal changes needed.

Think of it like spell-check - when you type a document, spell-check doesn't re-read the entire document every keystroke. It only checks what changed.

React uses three main strategies:

  1. Type checking: If component type changes (div β†’ span), React destroys and recreates it
  2. Keys: Help React track list items across renders, like ID badges for components
  3. Batching: Groups multiple updates into one render cycle, like processing all emails at once instead of one at a time

For example, [give the shopping list with keys example]. This is why we always need unique keys in lists - it helps React reconcile efficiently."

Q: "Why does React use Virtual DOM?"

Template Answer:

"Virtual DOM is a lightweight JavaScript copy of the real DOM. React uses it because direct DOM manipulation is expensive - like repainting your entire house every time you move a picture frame.

Here's how it works:

  1. When state changes, React creates a new Virtual DOM tree (fast - it's just JavaScript objects)
  2. React compares new Virtual DOM with old Virtual DOM (diffing)
  3. React calculates minimal DOM changes needed
  4. React updates only those specific parts of real DOM

It's like using a shopping list (Virtual DOM) before going to the store (real DOM). You compare your pantry with your list, then only buy what's missing. Much faster than randomly browsing every aisle!

The tradeoff is extra memory for the Virtual DOM, but it's worth it because DOM operations are ~10-100Γ— slower than JavaScript operations."

Q: "When should you NOT use index as key?"

Template Answer:

"Never use index as key when your list can change - items added, removed, or reordered. Here's why:

Imagine a classroom where students sit by row number (index). If a new student joins at the front:

  • Old Row 1: Alice

  • Old Row 2: Bob

  • New Row 1: Charlie (new student)

  • New Row 2: Alice (moved from Row 1)

  • New Row 3: Bob (moved from Row 2)

React thinks Alice and Bob are new students because their row numbers changed! It destroys their state and recreates them.

If students had ID badges (stable keys), React would know:

  • ID #123 (Alice) just moved rows
  • ID #456 (Bob) just moved rows
  • ID #789 (Charlie) is new

React would only create Charlie and move the others. Much more efficient!

Safe to use index: Static lists that never change Use unique IDs: Dynamic lists (filters, sorts, CRUD operations)"

</details>


Question 3: React 18 Concurrent Features​

Difficulty: πŸ”΄ Hard Frequency: ⭐⭐⭐⭐ Time: 10 minutes Companies: Meta, Modern React shops

Question​

What are React 18's concurrent features? How do useTransition, useDeferredValue, and Suspense work together?

Answer​

Concurrent React - Allows React to prepare multiple versions of the UI simultaneously and interrupt rendering to handle urgent updates.

Key Points:

  1. useTransition - Mark state updates as non-urgent (can be interrupted)
  2. useDeferredValue - Show stale value while new value is preparing
  3. Suspense - Declaratively handle async operations
  4. Automatic batching - All state updates are batched (even in promises/setTimeout)
  5. Streaming SSR - Send HTML to browser progressively

Code Example​

import { useState, useTransition, useDeferredValue, Suspense } from 'react';

// 1. useTransition - Non-urgent updates
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();

const handleSearch = (e) => {
const value = e.target.value;

// URGENT: Update input immediately (high priority)
setQuery(value);

// NON-URGENT: Search can wait (low priority)
startTransition(() => {
// This update can be interrupted by urgent updates
const filtered = expensiveSearchOperation(value);
setResults(filtered);
});
};

return (
<div>
<input value={query} onChange={handleSearch} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}

// 2. useDeferredValue - Defer expensive renders
function ProductGrid({ searchQuery }) {
// Deferred value "lags behind" the actual value
const deferredQuery = useDeferredValue(searchQuery);

// Input shows searchQuery (immediate)
// Grid shows deferredQuery (can lag behind)
return (
<div>
<h2>Searching for: {searchQuery}</h2>
{/* This expensive list uses deferred value */}
<ExpensiveProductList query={deferredQuery} />
</div>
);
}

// 3. COMBINING useTransition + useDeferredValue
function TabContainer() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();

const handleTabClick = (newTab) => {
startTransition(() => {
setTab(newTab);
});
};

return (
<div>
<TabBar tab={tab} onTabClick={handleTabClick} />
{/* Show old tab content while new tab is loading */}
<div style={{ opacity: isPending ? 0.5 : 1 }}>
<TabContent tab={tab} />
</div>
</div>
);
}

// 4. SUSPENSE FOR DATA FETCHING
// Resource-based data fetching (React 18+)
const resource = fetchUserData();

function ProfilePage() {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfileDetails resource={resource} />
<Suspense fallback={<PostsSkeleton />}>
<ProfilePosts resource={resource} />
</Suspense>
</Suspense>
);
}

function ProfileDetails({ resource }) {
// This "suspends" rendering until data is ready
const user = resource.user.read();
return <h1>{user.name}</h1>;
}

// 5. SUSPENSE + TRANSITIONS
function App() {
const [userId, setUserId] = useState(1);
const [isPending, startTransition] = useTransition();

const handleUserChange = (newId) => {
startTransition(() => {
// Navigation is non-urgent
setUserId(newId);
});
};

return (
<div>
<UserSelector onChange={handleUserChange} />
{/* Keep showing old user while new user loads */}
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
</div>
);
}

// 6. AUTOMATIC BATCHING (React 18)
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

// Before React 18: Only batched in React event handlers
// After React 18: Batched EVERYWHERE (promises, setTimeout, etc.)

const handleClick = () => {
// Both batched into one re-render
setCount(c => c + 1);
setFlag(f => !f);
};

const handleClickAsync = () => {
fetch('/api').then(() => {
// NOW BATCHED (wasn't in React 17)
setCount(c => c + 1);
setFlag(f => !f);
});
};

const handleClickTimeout = () => {
setTimeout(() => {
// NOW BATCHED (wasn't in React 17)
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);
};

return <button onClick={handleClick}>Update</button>;
}

// 7. STREAMING SSR
// server.js
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
// Send initial HTML shell immediately
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
// Components wrapped in Suspense stream in later
});
});

// 8. COMPARING APPROACHES

// Without Transition (blocking)
function SearchBlocking() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
// Blocks UI until search completes
setResults(expensiveSearch(value));
};

return <input value={query} onChange={handleChange} />;
// Input feels sluggish
}

// With Transition (non-blocking)
function SearchTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // Updates immediately
startTransition(() => {
setResults(expensiveSearch(value)); // Can be interrupted
});
};

return <input value={query} onChange={handleChange} />;
// Input stays responsive
}

Common Mistakes​

  • ❌ Not using startTransition for expensive non-urgent updates
  • ❌ Wrapping urgent updates in transitions (makes UI feel slow)
  • ❌ Expecting immediate updates inside startTransition
  • ❌ Not understanding automatic batching in React 18
  • βœ… Use transitions for navigation, filtering, searching
  • βœ… Keep urgent updates (user input) outside transitions
  • βœ… Combine Suspense + transitions for smooth UX

Follow-up Questions​

  1. When should you use useTransition vs useDeferredValue?
  2. How does Suspense know when to show fallback?
  3. What's the difference between batching in React 17 vs 18?

Resources​


<details> <summary><strong>πŸ” Deep Dive: React 18 Concurrent Rendering Architecture</strong></summary>

The Fundamental Shift: Cooperative Scheduling​

React 18's concurrent features represent a paradigm shift from synchronous to interruptible rendering. Previously, once React started rendering, it couldn't stop until complete. Concurrent React introduces lanes - priority-based update scheduling that allows high-priority updates to interrupt low-priority ones.

The Lane System (Simplified):

// React's internal priority lanes (31 total)
const SyncLane = 0b0000000000000000000000000000001; // Highest priority
const InputContinuousLane = 0b0000000000000000000000000000100;
const DefaultLane = 0b0000000000000000000000000010000;
const TransitionLane1 = 0b0000000000000000000000001000000; // Low priority
const IdleLane = 0b0100000000000000000000000000000; // Lowest priority

// Example: useTransition creates transition lane update
function useTransition() {
const [isPending, setIsPending] = useState(false);

const startTransition = (callback) => {
setIsPending(true);

// Schedule update with TransitionLane priority
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1; // Mark as transition

try {
setIsPending(false);
callback(); // All setState calls inside get TransitionLane
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
};

return [isPending, startTransition];
}

How Interruption Works:

// React's work loop (simplified)
function workLoopConcurrent() {
// Process units of work until time runs out OR higher priority work arrives
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}

// If interrupted (shouldYield = true), pause here
// Higher priority work can run
// Later, resume from workInProgress
}

function shouldYield() {
const currentTime = performance.now();

// Yield if we've used 5ms of frame time (60fps = 16.67ms per frame)
if (currentTime >= deadline) {
return true;
}

// Yield if higher priority update arrived
if (hasHigherPriorityWork()) {
return true;
}

return false;
}

// Example flow:
// 1. User clicks button β†’ TransitionLane update starts rendering
// 2. After 5ms, shouldYield() returns true β†’ pause rendering
// 3. Browser paints frame (UI stays responsive)
// 4. User types in input β†’ SyncLane update starts
// 5. SyncLane finishes quickly β†’ commits
// 6. Resume TransitionLane rendering

useTransition Implementation Details​

Internal Mechanism:

// How useTransition marks updates as non-urgent
function startTransition(callback) {
// 1. Mark all updates in callback as TransitionLane priority
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(TransitionLane);

// 2. Execute callback
// All setState calls inside inherit TransitionLane priority
try {
callback();
} finally {
// 3. Restore previous priority
setCurrentUpdatePriority(previousPriority);
}

// 4. Schedule render with TransitionLane
ensureRootIsScheduled(root);
}

// React schedules updates
function ensureRootIsScheduled(root) {
const nextLanes = getNextLanes(root); // Determines highest priority work

if (includesSyncLane(nextLanes)) {
// Sync work: schedule immediately (microtask)
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else if (includesNonIdleLane(nextLanes)) {
// Concurrent work: schedule with Scheduler
scheduleCallback(
priorityToSchedulerPriority(nextLanes),
performConcurrentWorkOnRoot.bind(null, root)
);
}
}

Why It's Non-Blocking:

// Example: Search with 10,000 items
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState(allItems);
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
const value = e.target.value;

// URGENT: Update input immediately (SyncLane)
setQuery(value);
// React commits this BEFORE starting expensive search

// NON-URGENT: Search can be interrupted (TransitionLane)
startTransition(() => {
const filtered = allItems.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setResults(filtered); // TransitionLane update
});
};

// Timeline:
// t=0ms: User types 'a'
// t=1ms: setQuery('a') commits β†’ input shows 'a' βœ…
// t=2ms: Start filtering 10,000 items
// t=5ms: Browser needs to paint β†’ React yields
// t=16ms: Frame painted, continue filtering
// t=50ms: Filtering complete β†’ commit results

return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}

useDeferredValue Deep Dive​

useDeferredValue creates a "lagging" copy of a value that updates only when React has idle time:

// React's internal implementation (simplified)
function useDeferredValue(value) {
const [deferredValue, setDeferredValue] = useState(value);

useEffect(() => {
// Schedule low-priority update for deferred value
startTransition(() => {
setDeferredValue(value);
});
}, [value]);

return deferredValue;
}

// Usage creates two-tier rendering
function ProductGrid({ searchQuery }) {
const deferredQuery = useDeferredValue(searchQuery);
// searchQuery = "lap" (user typing)
// deferredQuery = "la" (lagging behind)

return (
<div>
<h2>Search: {searchQuery}</h2> {/* Shows latest */}
<ExpensiveList query={deferredQuery} /> {/* Shows deferred */}
</div>
);
}

// Render timeline:
// t=0: searchQuery = "l", deferredQuery = ""
// - Render header with "l"
// - Render list with ""
// - Schedule deferred update

// t=50ms: User types "a" β†’ searchQuery = "la"
// - Re-render header with "la" (sync, instant)
// - deferredQuery still "" (transition not started)
// - Schedule deferred update for "la"

// t=100ms: User types "p" β†’ searchQuery = "lap"
// - Re-render header with "lap" (sync)
// - Previous deferred update CANCELED (value changed)
// - Schedule deferred update for "lap"

// t=200ms: User stops typing
// - Deferred update completes
// - deferredQuery = "lap"
// - Render expensive list

// Result: Input feels instant, expensive list updates once user pauses

Suspense for Data Fetching Architecture​

Suspense leverages JavaScript's throw/catch mechanism to "suspend" rendering:

// How Suspense works internally
function Suspense({ fallback, children }) {
try {
// Try to render children
return children;
} catch (promise) {
// If child throws a promise, catch it
if (isPromise(promise)) {
// Show fallback while promise pending
promise.then(() => {
// When promise resolves, re-render
forceUpdate();
});
return fallback;
}
throw promise; // Re-throw non-promises
}
}

// Resource-based data fetching
function createResource(promise) {
let status = 'pending';
let result;

const suspender = promise.then(
(data) => {
status = 'success';
result = data;
},
(error) => {
status = 'error';
result = error;
}
);

return {
read() {
if (status === 'pending') {
throw suspender; // Suspense catches this!
} else if (status === 'error') {
throw result;
}
return result;
}
};
}

// Usage
const userResource = createResource(fetchUser(userId));

function UserProfile() {
const user = userResource.read(); // Throws promise if pending
return <h1>{user.name}</h1>;
}

function App() {
return (
<Suspense fallback={<Skeleton />}>
<UserProfile /> {/* Suspends on first render */}
</Suspense>
);
}

// Render flow:
// 1. React renders <Suspense>
// 2. React renders <UserProfile>
// 3. userResource.read() called
// 4. status = 'pending' β†’ throw promise
// 5. Suspense catches promise β†’ shows <Skeleton>
// 6. Promise resolves β†’ React re-renders <UserProfile>
// 7. status = 'success' β†’ return user data
// 8. Suspense shows <UserProfile> with data

Automatic Batching Mechanism​

React 18 extends batching to ALL contexts using microtasks:

// React 17: Only batches in event handlers
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// βœ… Batched (one re-render)
}

setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// ❌ NOT batched (two re-renders)
}, 1000);

// React 18: Batches everywhere
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// βœ… Batched (one re-render)
}, 1000);

// How it works (simplified)
let updateQueue = [];
let isScheduled = false;

function scheduleUpdate(fiber, update) {
updateQueue.push({ fiber, update });

if (!isScheduled) {
isScheduled = true;

// Use microtask to batch all synchronous updates
queueMicrotask(() => {
flushUpdates();
isScheduled = false;
});
}
}

function flushUpdates() {
const updates = updateQueue.splice(0);

// Apply all updates
updates.forEach(({ fiber, update }) => {
applyUpdate(fiber, update);
});

// Single render for all updates
render(rootFiber);
}

// Timeline:
// t=0: setTimeout callback starts
// t=0: setCount called β†’ scheduleUpdate β†’ queue microtask
// t=0: setFlag called β†’ scheduleUpdate (microtask already queued)
// t=0: setTimeout callback ends
// t=1: Microtask runs β†’ flushUpdates() β†’ ONE render

Streaming SSR with Suspense​

React 18 allows server-side HTML streaming with selective hydration:

// Server (Node.js)
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
const { pipe, abort } = renderToPipeableStream(
<App />,
{
// Send initial HTML shell immediately
onShellReady() {
res.setHeader('Content-Type', 'text/html');
pipe(res);
},

// Handle errors
onShellError(error) {
res.statusCode = 500;
res.send('<!doctype html><p>Server Error</p>');
},

// All content ready
onAllReady() {
// Could wait here for full render, but onShellReady is faster
}
}
);

setTimeout(abort, 10000); // Abort after 10s
});

// App with streaming boundaries
function App() {
return (
<html>
<head><title>My App</title></head>
<body>
<Header />

{/* Main content streams immediately */}
<MainContent />

{/* Comments stream when ready */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>

{/* Recommendations stream when ready */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</body>
</html>
);
}

// Server sends HTML in chunks:
// Chunk 1 (immediate):
// <html><head>...</head><body>
// <header>...</header>
// <main>...</main>
// <div id="suspense-1"><!--$?--><div>Loading comments...</div><!--/$--></div>
// <div id="suspense-2"><!--$?--><div>Loading recommendations...</div><!--/$--></div>

// Chunk 2 (when Comments ready):
// <div hidden id="comments-data">
// <div>Actual comments HTML...</div>
// </div>
// <script>
// // Replace skeleton with real content
// document.getElementById('suspense-1').replaceWith(
// document.getElementById('comments-data').firstChild
// );
// </script>

// Chunk 3 (when Recommendations ready):
// <div hidden id="recs-data">
// <div>Actual recommendations HTML...</div>
// </div>
// <script>
// document.getElementById('suspense-2').replaceWith(
// document.getElementById('recs-data').firstChild
// );
// </script>
// </body></html>

Selective Hydration:

// Client hydrates in priority order
import { hydrateRoot } from 'react-dom/client';

// React 18 selective hydration
hydrateRoot(document, <App />);

// Hydration order:
// 1. User clicks on <Comments> while still hydrating
// 2. React PRIORITIZES hydrating <Comments> first (user intent)
// 3. Other sections hydrate after
// 4. Result: Interactive content ASAP

// React 17: All-or-nothing hydration
// Must hydrate entire app before ANY interactivity

Performance Optimization with Concurrent Features​

// Combining all features for optimal UX
function OptimizedApp() {
const [tab, setTab] = useState('home');
const [searchQuery, setSearchQuery] = useState('');
const [isPending, startTransition] = useTransition();
const deferredQuery = useDeferredValue(searchQuery);

const handleTabChange = (newTab) => {
startTransition(() => {
setTab(newTab); // Non-urgent navigation
});
};

const handleSearch = (e) => {
setSearchQuery(e.target.value); // Urgent: update input
// deferredQuery updates non-urgently
};

return (
<div>
{/* Sync updates: instant */}
<Tabs current={tab} onChange={handleTabChange} />
<SearchInput value={searchQuery} onChange={handleSearch} />

{/* Show loading state during transitions */}
{isPending && <GlobalSpinner />}

{/* Suspense boundaries for data */}
<Suspense fallback={<TabSkeleton />}>
{/* Tab content uses deferred query */}
<TabContent tab={tab} query={deferredQuery} />
</Suspense>
</div>
);
}

// Performance characteristics:
// - Input typing: 0ms delay (sync updates)
// - Tab switching: Smooth with loading indicator (transitions)
// - Search results: Update when user pauses (deferred values)
// - Data loading: Progressive with skeletons (Suspense)

</details>


<details> <summary><strong>πŸ› Real-World Scenario: Concurrent Features Migration</strong></summary>

Production Case Study: Dashboard Performance Overhaul​

Context: Enterprise analytics dashboard with 50+ charts, real-time data updates, and complex user interactions experiencing severe performance issues after scaling to 100K+ users.

Symptoms:

  • Tab switching taking 3-5 seconds to respond
  • Search input freezing during filtering
  • Charts not updating when users change date ranges
  • INP (Interaction to Next Paint) score: 850ms (Poor)
  • User complaints: "Dashboard feels broken"

Initial Implementation (React 17):

// ❌ PROBLEM CODE - Blocking updates
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [dateRange, setDateRange] = useState('7d');
const [searchQuery, setSearchQuery] = useState('');
const [chartData, setChartData] = useState([]);

// MISTAKE 1: Synchronous state updates block UI
const handleTabChange = (tab) => {
setActiveTab(tab);
// Causes full re-render of 50+ charts immediately
};

// MISTAKE 2: Filtering blocks input
const handleSearch = (e) => {
const value = e.target.value;
setSearchQuery(value);

// Processes 10,000 items synchronously
const filtered = chartData.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setChartData(filtered);
};

// MISTAKE 3: Waterfall loading
useEffect(() => {
fetchOverviewData().then(setOverviewData);
fetchChartData().then(setChartData);
fetchMetrics().then(setMetrics);
// All load sequentially, blocking each other
}, [activeTab, dateRange]);

return (
<div>
<Tabs value={activeTab} onChange={handleTabChange} />
<DateRangePicker value={dateRange} onChange={setDateRange} />
<SearchInput value={searchQuery} onChange={handleSearch} />

{/* All charts render synchronously */}
<div className="charts">
{chartData.map(data => (
<Chart key={data.id} data={data} />
))}
</div>
</div>
);
}

Performance Metrics Before Fix:

Tab switch response: 3200ms
Search input delay: 450ms per keystroke
Date range change: 2800ms
INP score: 850ms (Poor)
TBT (Total Blocking Time): 4500ms
Largest Contentful Paint: 5.2s
User-reported "freeze" incidents: 340/week

Root Cause Analysis:

  1. Synchronous Tab Switching:
// When user clicks tab:
setActiveTab('analytics')
β†’ React starts rendering entire Analytics tab (50 charts)
β†’ Main thread blocked for 3200ms
β†’ UI frozen
β†’ User sees nothing happening
β†’ Clicks again (duplicate requests)
  1. Blocking Search:
// Every keystroke:
setSearchQuery('a')
β†’ Filter 10,000 items
β†’ Re-render all filtered charts
β†’ 450ms blocking time
β†’ Input feels laggy
β†’ Users type slower or give up
  1. Sequential Data Loading:
// Waterfall effect:
t=0ms: fetchOverviewData() starts
t=500ms: fetchOverviewData() completes
t=500ms: fetchChartData() starts
t=1200ms: fetchChartData() completes
t=1200ms: fetchMetrics() starts
t=1800ms: fetchMetrics() completes
// Total: 1800ms when could be 500ms parallel

Solution: React 18 Concurrent Features:

// βœ… FIXED CODE - Non-blocking with concurrent features

import { useState, useTransition, useDeferredValue, Suspense } from 'react';
import { useQuery } from '@tanstack/react-query';

function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [dateRange, setDateRange] = useState('7d');
const [searchQuery, setSearchQuery] = useState('');

// FIX 1: Use transition for non-urgent tab switching
const [isPending, startTransition] = useTransition();

// FIX 2: Defer expensive search filtering
const deferredSearchQuery = useDeferredValue(searchQuery);

const handleTabChange = (newTab) => {
startTransition(() => {
setActiveTab(newTab);
// Update is non-urgent, won't block UI
});
};

const handleSearch = (e) => {
// Urgent: Update input immediately
setSearchQuery(e.target.value);
// deferredSearchQuery updates non-urgently
};

return (
<div>
{/* These update instantly (sync) */}
<Tabs value={activeTab} onChange={handleTabChange} />
<DateRangePicker value={dateRange} onChange={setDateRange} />
<SearchInput value={searchQuery} onChange={handleSearch} />

{/* Show loading state during transitions */}
{isPending && <LinearProgress />}

{/* FIX 3: Suspense boundaries for parallel loading */}
<Suspense fallback={<DashboardSkeleton />}>
<TabContent
tab={activeTab}
dateRange={dateRange}
searchQuery={deferredSearchQuery}
/>
</Suspense>
</div>
);
}

// Separate component for tab content
function TabContent({ tab, dateRange, searchQuery }) {
// FIX 4: Parallel data fetching with Suspense
const overviewData = useSuspenseQuery({
queryKey: ['overview', tab, dateRange],
queryFn: () => fetchOverviewData(tab, dateRange)
});

const chartData = useSuspenseQuery({
queryKey: ['charts', tab, dateRange],
queryFn: () => fetchChartData(tab, dateRange)
});

const metrics = useSuspenseQuery({
queryKey: ['metrics', tab, dateRange],
queryFn: () => fetchMetrics(tab, dateRange)
});

// FIX 5: Use deferred value for expensive filtering
const filteredCharts = useMemo(() => {
if (!searchQuery) return chartData.data;

return chartData.data.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [chartData.data, searchQuery]); // Uses deferred value

return (
<div>
<OverviewSection data={overviewData.data} />
<MetricsGrid data={metrics.data} />

{/* FIX 6: Nested Suspense for progressive loading */}
<Suspense fallback={<ChartsSkeleton count={filteredCharts.length} />}>
<ChartsGrid charts={filteredCharts} />
</Suspense>
</div>
);
}

// FIX 7: Virtualize large chart lists
import { FixedSizeGrid } from 'react-window';

function ChartsGrid({ charts }) {
return (
<FixedSizeGrid
height={800}
width={1200}
columnCount={3}
rowCount={Math.ceil(charts.length / 3)}
columnWidth={400}
rowHeight={300}
>
{({ columnIndex, rowIndex, style }) => {
const index = rowIndex * 3 + columnIndex;
const chart = charts[index];
return chart ? (
<div style={style}>
<Chart data={chart} />
</div>
) : null;
}}
</FixedSizeGrid>
);
}

Performance Metrics After Fix:

Tab switch response: 180ms (94% improvement)
Search input delay: 8ms per keystroke (98% improvement)
Date range change: 220ms (92% improvement)
INP score: 95ms (Good - 89% improvement)
TBT: 280ms (94% improvement)
LCP: 1.8s (65% improvement)
User-reported "freeze" incidents: 12/week (96% reduction)

Key Improvements Breakdown:

  1. useTransition for Tab Switching:
// Before: Blocking
handleTabChange('analytics')
β†’ 3200ms freeze
β†’ User frustrated

// After: Non-blocking
startTransition(() => setActiveTab('analytics'))
β†’ Tab button responds in 50ms (visual feedback)
β†’ Content updates in 180ms (interrupting low-pri work)
β†’ User sees progress bar
β†’ Feels responsive
  1. useDeferredValue for Search:
// Before: Every keystroke blocks
User types: "a" β†’ "an" β†’ "ana" β†’ "anal" β†’ "analy"
Each keystroke: 450ms filter + render = 2250ms total blocking

// After: Deferred filtering
User types: "a" β†’ "an" β†’ "ana" β†’ "anal" β†’ "analy"
Input updates: 8ms Γ— 5 = 40ms (responsive)
Filter runs ONCE when user pauses: 180ms
Total: 220ms vs 2250ms (10Γ— better UX)
  1. Suspense for Parallel Loading:
// Before: Sequential waterfall
fetchOverviewData() β†’ 500ms
fetchChartData() β†’ 700ms
fetchMetrics() β†’ 600ms
Total: 1800ms

// After: Parallel with Suspense
Promise.all([
fetchOverviewData(), // 500ms
fetchChartData(), // 700ms
fetchMetrics() // 600ms
])
Total: 700ms (max of all)
Improvement: 61% faster

Debugging Process:

// 1. React DevTools Profiler - identified long renders
// Saw 3200ms render time for tab switch

// 2. Wrapped slow updates in transitions
console.time('tab-switch');
startTransition(() => setActiveTab('analytics'));
console.timeEnd('tab-switch'); // 180ms

// 3. Used Concurrent Mode flag
if (isPending) {
console.log('Transition in progress');
}

// 4. Monitored Suspense boundaries
<Suspense fallback={<Skeleton />}>
<TabContent /> {/* Logs when suspends/resumes */}
</Suspense>

// 5. Performance.measure for INP
performance.mark('interaction-start');
// ... user interaction
performance.mark('interaction-end');
performance.measure('INP', 'interaction-start', 'interaction-end');

Business Impact:

  • User engagement increased 34% (less frustration)
  • Average session time increased 28%
  • Dashboard exports increased 42% (users completed tasks)
  • Customer support tickets reduced 67%
  • Enterprise upsells increased 19% (better product perception)

Migration Challenges:

  1. Testing Async Behavior:
// Old test (React 17):
render(<Dashboard />);
fireEvent.click(screen.getByText('Analytics'));
expect(screen.getByText('Chart 1')).toBeInTheDocument();

// New test (React 18 with transitions):
render(<Dashboard />);
fireEvent.click(screen.getByText('Analytics'));
// FAILS! Transition is async

// Fixed test:
render(<Dashboard />);
await act(async () => {
fireEvent.click(screen.getByText('Analytics'));
await waitFor(() => {
expect(screen.getByText('Chart 1')).toBeInTheDocument();
});
});
  1. Third-Party Libraries: Some chart libraries didn't work well with concurrent rendering initially. Solution: Wrapped in useSyncExternalStore:
import { useSyncExternalStore } from 'react';

function useChartLibrary(data) {
return useSyncExternalStore(
chartLib.subscribe,
() => chartLib.getData(),
() => chartLib.getServerSnapshot()
);
}

Lessons Learned:

  1. Start with useTransition for non-urgent updates (tabs, filters, sorts)
  2. useDeferredValue for expensive renders triggered by user input
  3. Suspense boundaries for data fetching (parallel > sequential)
  4. Combine with virtualization for large lists
  5. Monitor isPending to show loading states
  6. Test async behavior properly

</details>


<details> <summary><strong>βš–οΈ Trade-offs: Concurrent Features vs Traditional Approaches</strong></summary>

useTransition vs Manual Debouncing​

Traditional Debouncing:

// Delay updates until user stops typing
function SearchWithDebounce() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

const debouncedSearch = useMemo(
() => debounce((value) => {
const filtered = expensiveSearch(value);
setResults(filtered);
}, 300),
[]
);

const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};

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

// Pros:
// + Simple to understand
// + Works in React 17
// + Reduces number of operations

// Cons:
// - Fixed delay (300ms feels arbitrary)
// - Delay feels laggy if device is fast
// - No visual loading indicator
// - Can't interrupt once started
// - Results from old queries can arrive late

useTransition Approach:

function SearchWithTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
const value = e.target.value;
setQuery(value);

startTransition(() => {
const filtered = expensiveSearch(value);
setResults(filtered);
});
};

return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}

// Pros:
// + Automatic timing (React decides when to yield)
// + Built-in loading state (isPending)
// + Interruptible (new search cancels old)
// + No artificial delay on fast devices
// + Latest result always wins

// Cons:
// - Requires React 18
// - More complex mental model
// - Can't control exact timing

Performance Comparison (1000-item search):

Metric | Debounce | useTransition
--------------------------|----------|---------------
Input responsiveness | Instant | Instant
Time to first result | 300ms+ | 50-150ms
Fast device experience | Delayed | Immediate
Slow device experience | Same | Progressive
Loading indicator | Manual | Built-in
Stale results | Possible | Prevented

When to use each:

  • Debounce: API calls, expensive operations that shouldn't run multiple times, React 17 projects
  • useTransition: UI updates, filtering, sorting, navigation, when you want automatic optimization

useDeferredValue vs Throttling​

Traditional Throttling:

function GridWithThrottle({ searchQuery }) {
const [displayQuery, setDisplayQuery] = useState(searchQuery);

useEffect(() => {
const throttled = throttle(() => {
setDisplayQuery(searchQuery);
}, 200);

throttled();
return () => throttled.cancel();
}, [searchQuery]);

return <ExpensiveGrid query={displayQuery} />;
}

// Pros:
// + Limits render frequency
// + Works in React 17

// Cons:
// - Fixed interval (200ms)
// - Renders even if not needed
// - No priority system

useDeferredValue Approach:

function GridWithDeferred({ searchQuery }) {
const deferredQuery = useDeferredValue(searchQuery);

return (
<div>
<h2>{searchQuery}</h2> {/* Latest */}
<ExpensiveGrid query={deferredQuery} /> {/* Deferred */}
</div>
);
}

// Pros:
// + Automatic scheduling
// + Skips unnecessary renders
// + Lower priority than urgent updates
// + Shows both values

// Cons:
// - Requires React 18
// - Can lag significantly under load

Comparison (rapid typing):

// Throttle: Renders at fixed intervals
User types: "a" "b" "c" "d" "e" at t=0,50,100,150,200ms
Throttle (200ms): Renders at t=200 ("e") βœ…

User types: "a" "b" "c" "d" "e" "f" at t=0,50,100,150,200,250ms
Throttle: Renders at t=200 ("e"), t=400 ("f")
Result: Extra render for "e" (user already typed "f")

// useDeferredValue: Skips intermediate values
User types: "a" "b" "c" "d" "e" at t=0,50,100,150,200ms
Deferred: Waits for idle time, renders only "e" βœ…

User types: "a" "b" "c" "d" "e" "f" at t=0,50,100,150,200,250ms
Deferred: Renders only "f" (skipped "e") βœ…

Suspense vs Traditional Loading States​

Traditional Loading States:

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);

if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
return <div>{user.name}</div>;
}

// Pros:
// + Explicit control
// + Works anywhere
// + Easy to debug

// Cons:
// - Manual state management
// - Loading waterfalls
// - No coordination between components
// - Loading state duplication

Suspense Approach:

const userResource = createResource(userId => fetchUser(userId));

function UserProfile({ userId }) {
const user = userResource(userId).read(); // Suspends if loading
return <div>{user.name}</div>;
}

function App() {
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userId={123} />
</Suspense>
);
}

// Pros:
// + Declarative loading states
// + Automatic coordination
// + Prevents waterfalls
// + Cleaner component code

// Cons:
// - Requires resource pattern
// - Less control over loading states
// - Error boundaries needed for errors

Waterfall Prevention:

// Traditional: Sequential loading
function Dashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);

useEffect(() => {
fetchUser().then(u => {
setUser(u);
// Can only fetch posts after user loaded
fetchPosts(u.id).then(setPosts);
});
}, []);

// t=0-500ms: Fetch user
// t=500-1200ms: Fetch posts
// Total: 1200ms
}

// Suspense: Parallel loading
const userResource = createResource(fetchUser());
const postsResource = createResource(fetchPosts());

function Dashboard() {
const user = userResource.read(); // Starts immediately
const posts = postsResource.read(); // Starts immediately

// t=0-700ms: Both fetch in parallel
// Total: 700ms (max of both)
}

Decision Matrix​

Use useTransition when:

  • You control the state update (have access to setState)
  • Update is non-urgent (navigation, filtering, sorting)
  • Want built-in loading indicator
  • Need to interrupt expensive renders

Use useDeferredValue when:

  • Receiving value from props/parent
  • Want to defer expensive child renders
  • Need to show both current and deferred values
  • Can't modify parent component

Use Suspense when:

  • Loading data from async sources
  • Want declarative loading states
  • Need to coordinate multiple async operations
  • Building data-driven UIs

Combine all three for optimal UX:

function OptimalDashboard() {
const [tab, setTab] = useState('home');
const [search, setSearch] = useState('');
const [isPending, startTransition] = useTransition();
const deferredSearch = useDeferredValue(search);

const handleTabChange = (newTab) => {
startTransition(() => setTab(newTab));
};

return (
<div>
<Tabs value={tab} onChange={handleTabChange} />
<SearchInput value={search} onChange={setSearch} />
{isPending && <ProgressBar />}

<Suspense fallback={<ContentSkeleton />}>
<TabContent tab={tab} search={deferredSearch} />
</Suspense>
</div>
);
}

// Benefits:
// - Instant input feedback
// - Non-blocking navigation
// - Deferred expensive filtering
// - Parallel data loading
// - Coordinated loading states

</details>


<details> <summary><strong>πŸ’¬ Explain to Junior: React 18 Concurrent Features Like You're Five</strong></summary>

The Restaurant Analogy​

Imagine you're a chef in a busy restaurant. Orders keep coming in, and you need to cook efficiently.

React 17 (Synchronous) - One Order at a Time:

// Customer orders:
1. "Make pizza" (takes 20 minutes)
2. "Make salad" (takes 2 minutes)
3. "Refill water" (takes 30 seconds)

// React 17 approach:
1. Start making pizza
2. 20 minutes later, pizza done
3. Start making salad
4. 2 minutes later, salad done
5. Refill water
6. 30 seconds later, water done

// Problem: Customer waited 22.5 minutes for water!

React 18 Concurrent - Priority-Based Cooking:

// Same orders, but chef notices water is urgent:
1. Start making pizza
2. After 5 minutes, customer asks for water (URGENT!)
3. PAUSE pizza, refill water (30 seconds)
4. Resume pizza
5. While pizza cooks, quickly make salad
6. Serve everything efficiently

// Result: Water in 5.5 minutes, not 22.5!
// This is how useTransition works

The GPS Navigation Metaphor (useTransition)​

You're driving and using GPS:

// WITHOUT useTransition:
You type destination: "S" β†’ "St" β†’ "Star" β†’ "Starb" β†’ "Starbucks"

// Phone behavior:
Type "S" β†’ Calculate ALL routes for "S" (blocks for 2 seconds)
Type "t" β†’ Calculate ALL routes for "St" (blocks for 2 seconds)
Type "a" β†’ Calculate ALL routes for "Sta" (blocks for 2 seconds)

// Result: Phone freezes on every keystroke!

// WITH useTransition:
You type: "S" β†’ "St" β†’ "Star" β†’ "Starb" β†’ "Starbucks"

// Phone behavior:
Type "S" β†’ Show letter "S" immediately (urgent)
β†’ Start calculating routes (non-urgent, can pause)
Type "t" β†’ Show "St" immediately (urgent)
β†’ CANCEL route calculation for "S"
β†’ Start calculating "St" routes
Type "a" β†’ Show "Sta" immediately
β†’ CANCEL "St" calculation
β†’ Start "Sta" calculation

// Result: Typing feels instant, routes calculated when you stop!
// This is exactly what useTransition does

The TV Remote Analogy (useDeferredValue)​

You're watching TV with a slow remote:

// Regular remote (immediate):
Press "3" β†’ TV changes to channel 3 immediately
Press "2" β†’ TV changes to channel 2 immediately

// Deferred remote (smart):
Press "3" β†’ Display shows "3" immediately
β†’ TV changes to channel 3 when ready
Press "2" β†’ Display shows "2" immediately (cancels channel 3)
β†’ TV changes to channel 2 when ready

// Benefit: Display feels instant, TV updates when it can
// This is useDeferredValue
// Code example
function TVControls() {
const [channel, setChannel] = useState(1);
const deferredChannel = useDeferredValue(channel);

return (
<div>
<h2>Display: {channel}</h2> {/* Instant */}
<ExpensiveTV channel={deferredChannel} /> {/* Deferred */}
</div>
);
}

The Museum Analogy (Suspense)​

You're visiting a museum with different rooms:

// OLD WAY (traditional loading):
Guide says: "Wait in lobby until ALL rooms are ready"
β†’ Room 1 setup: 5 minutes
β†’ Room 2 setup: 10 minutes
β†’ Room 3 setup: 3 minutes
Total wait: 18 minutes before seeing ANYTHING

// SUSPENSE WAY:
Guide says: "Each room shows skeleton/placeholder first"
β†’ Room 1: Show empty room immediately, paintings appear in 5 min
β†’ Room 2: Show empty room immediately, sculptures appear in 10 min
β†’ Room 3: Show empty room immediately, photos appear in 3 min

You can START exploring Room 1 at t=0
Room 3 ready at t=3 min
Room 1 fully ready at t=5 min
Room 2 fully ready at t=10 min

// Benefit: You're exploring instead of waiting in lobby!
// Code example
function Museum() {
return (
<div>
<Suspense fallback={<RoomSkeleton />}>
<Room1 /> {/* Loads independently */}
</Suspense>

<Suspense fallback={<RoomSkeleton />}>
<Room2 /> {/* Loads independently */}
</Suspense>

<Suspense fallback={<RoomSkeleton />}>
<Room3 /> {/* Loads independently */}
</Suspense>
</div>
);
}

Interview Answer Templates​

Q: "What is useTransition and when would you use it?"

Template Answer:

"useTransition lets you mark state updates as non-urgent, so React can interrupt them if something more important happens - like user input.

Think of it like a chef who can pause making a cake if a customer needs water. The water (urgent) gets served first, then the chef resumes the cake (non-urgent).

I use it for:

  • Tab switching (navigation is non-urgent)
  • Search filtering (results can wait, input must be instant)
  • Sorting large lists (UI shouldn't freeze)

Example: [Give GPS navigation analogy]. The key benefit is the UI stays responsive - users can type, click, scroll without waiting for expensive renders to finish.

The hook gives you isPending so you can show loading indicators. This makes the app feel faster even though the work takes the same time."

Q: "What's the difference between useTransition and useDeferredValue?"

Template Answer:

"Both handle non-urgent updates, but they work differently:

useTransition: You control the state update

  • Use when you call setState yourself
  • Example: Button clicks, form submissions

useDeferredValue: You receive value from props

  • Use when parent controls the value
  • Example: Expensive child component receiving search query

Real-world analogy: useTransition is like saying 'Do this task later (startTransition)'. useDeferredValue is like saying 'I'll use the old value until you're done with the new one'.

Code example:

// useTransition (I control state)
const [isPending, startTransition] = useTransition();
startTransition(() => setResults(filtered));

// useDeferredValue (parent controls value)
function Child({ searchQuery }) {
const deferred = useDeferredValue(searchQuery);
return <List query={deferred} />;
}

Choose useTransition when possible - it's more direct and gives you isPending."

Q: "How does Suspense work?"

Template Answer:

"Suspense is like a loading boundary that says 'Show a fallback while this content loads'. It catches promises from child components and shows a loading state.

Think of it like a museum with placeholder signs. When you walk in, each room shows a sign saying 'Paintings arriving soon' until the actual paintings arrive.

How it works technically:

  1. Child component 'throws' a promise if data not ready
  2. Suspense catches that promise
  3. Suspense shows fallback (skeleton/spinner)
  4. When promise resolves, Suspense shows actual content

Benefits over traditional loading states:

  • Declarative (just wrap components)
  • Coordinates multiple async operations
  • Prevents waterfalls (loads in parallel)
  • Cleaner code (no manual loading/error state)

Example: Instead of each component managing isLoading states, Suspense handles it:

<Suspense fallback={<Skeleton />}>
<UserProfile /> {/* May suspend */}
<Posts /> {/* May suspend */}
</Suspense>

Both load in parallel, single loading state, automatic coordination."

Q: "What's automatic batching in React 18?"

Template Answer:

"Automatic batching means React groups multiple state updates into one re-render, everywhere - not just in event handlers like React 17.

Analogy: It's like collecting all your mail at once instead of running to the mailbox every time a letter arrives.

Before React 18:

  • Batched in React events: onClick, onChange βœ…
  • NOT batched in: setTimeout, fetch callbacks, promises ❌

After React 18:

  • Batched EVERYWHERE βœ…

Example:

fetch('/api/data').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 17: Two re-renders
// React 18: One re-render (batched)
});

This makes apps faster automatically without code changes. The benefits are fewer renders, better performance, less layout thrashing."

</details>


</details>


← Back to React README


Source: internals-02-reconciliation.md