React

Virtual DOM & Reconciliation

14 Questions

The Virtual DOM is a lightweight JavaScript object representation of the real DOM. When state changes, React creates a new virtual tree, diffs it against the previous one using its reconciliation algorithm, and applies only the minimum necessary changes to the real DOM. Direct DOM manipulation is expensive because browsers must recompute layouts; batching changes through the virtual DOM makes complex UI updates significantly faster and more predictable.

Why it matters: Understanding the Virtual DOM explains why React is fast and how it can update the UI efficiently without full page refreshes.

Real applications: Every React app — the Virtual DOM is the foundation of React's rendering system, making complex dynamic UIs practical.

Common mistakes: Thinking the Virtual DOM is always faster than direct DOM updates — for simple, infrequent updates, direct DOM manipulation can be faster.

Reconciliation is React's diffing algorithm for comparing two virtual DOM trees. It applies two key heuristics: elements of different types produce completely different trees (full remount), and developers provide key props to help React identify stable elements across renders. These heuristics reduce the O(n³) general tree-diff problem to O(n), making reconciliation fast enough to run on every state change.

Why it matters: Reconciliation is what makes React efficient — it finds the minimal set of DOM changes needed instead of redrawing everything.

Real applications: Every state update triggers reconciliation. Understanding it helps you write components that diff efficiently and avoid unnecessary remounts.

Common mistakes: Changing a component's type conditionally (e.g., rendering a <div> sometimes and a <section> other times) — this forces a full remount every time.

React Fiber is React's reconciliation engine introduced in React 16. It breaks rendering work into incremental units that can be paused, resumed, and prioritized, rather than processing the entire tree synchronously in one blocking pass. This architecture is what enables Suspense, concurrent rendering, and time-slicing, allowing React to keep the UI responsive during expensive renders by yielding control back to the browser between work units.

Why it matters: Fiber makes React apps feel responsive even during expensive rendering — React can interrupt and prioritize user interactions over background work.

Real applications: Suspense for data loading, startTransition for non-urgent updates, concurrent features that keep the UI interactive during heavy computation.

Common mistakes: Not using startTransition for slow non-urgent updates, causing the UI to feel frozen while React does heavy rendering work.

Keys help React identify which items in a list changed, were added, or removed during reconciliation. Without keys, React falls back to a positional comparison and re-renders all items on any list change. With stable, unique keys, React can efficiently reorder, add, or remove individual items while preserving the state of unchanged ones.

items.map(item => <li key={item.id}>{item.name}</li>)

Never use array index as key if the list can reorder.

Why it matters: Keys let React identify which items changed. Without them, React can't efficiently update lists and may reset state on wrong elements.

Real applications: Any list rendered with .map() — to-do items, product cards, table rows, comment threads.

Common mistakes: Using the array index as a key for a list that can be sorted or filtered — React matches elements by key, so index keys cause wrong elements to receive old state.

When React encounters different element types at the same position in the tree, it tears down the entire old subtree and builds a new one from scratch — all state is lost. For example, switching from <div> to <span> unmounts the div and everything inside it, regardless of how similar the children are. This is why conditionally swapping a component type causes it to reset, and why using a shared key is the correct way to preserve state across type changes.

Why it matters: A type change causes a full remount — all child state is destroyed. This can cause unexpected resets if you're not careful.

Real applications: Toggling between an <input> and a <p> for an editable field — React remounts and loses any internal state.

Common mistakes: Conditionally rendering different wrapper types at the same position, causing input state, focus, or animations to reset unexpectedly.

React 18 introduced automatic batching for all state updates, including those inside Promise callbacks, setTimeout, and native event handlers — which were previously processed one at a time. Multiple setState calls in the same synchronous block are now batched into a single re-render, reducing unnecessary work. If you ever need to opt out, wrap updates in ReactDOM.flushSync() to force immediate individual renders.

Why it matters: Automatic batching in React 18 prevents unnecessary intermediate renders when multiple pieces of state update together.

Real applications: A form submit handler that validates and sets multiple state values — all updates batch into one re-render instead of one per state call.

Common mistakes: Expecting each setState call to cause a separate render immediately — React batches them and renders once at the end of the event.

Concurrent rendering (React 18+) allows React to prepare multiple versions of the UI simultaneously and interrupt an in-progress render when a higher-priority update arrives. This keeps the UI responsive during expensive computations by yielding control to the browser between work units. Features like useTransition and useDeferredValue let you mark updates as non-urgent so React renders the current UI first and processes the expensive update in the background.

Why it matters: Concurrent mode lets React keep the UI responsive even while doing expensive rendering work in the background.

Real applications: Typing in a search input while results are loading, keeping animations smooth while a heavy list re-renders.

Common mistakes: Not using startTransition for non-urgent state updates (like filtering a large list) — the UI feels frozen while React renders the new results.

When a component re-renders, React: 1) Calls the component function to get new JSX, 2) Diffs the new virtual DOM against the previous snapshot, 3) Computes the minimal DOM changes required, 4) Commits changes to the real DOM. Child components re-render whenever their parent re-renders unless they are wrapped in React.memo. The render phase is pure and can be interrupted; the commit phase applies DOM changes and runs effects and cannot be interrupted.

Why it matters: Understanding React's update cycle helps you reason about what causes re-renders and where React actually touches the DOM.

Real applications: Debugging why a component re-renders unexpectedly, understanding the order of effects, predicting when the DOM will update.

Common mistakes: Assuming React updates the DOM immediately after setState — it batches and applies changes at the end of the cycle, not right away.

Virtual DOM is a React concept — a JavaScript object tree used for diffing to minimize real DOM updates. Shadow DOM is a browser API for creating encapsulated DOM subtrees with scoped CSS, used by Web Components to prevent style leakage. They are completely unrelated technologies solving different problems and can coexist in the same application.

Why it matters: Confusing the two leads to misunderstanding what React does vs what Web Components do — they're different tools for different purposes.

Real applications: Virtual DOM is React's internal optimization; Shadow DOM is used by native browser components like <video> controls or custom elements.

Common mistakes: Thinking React uses browser's Shadow DOM internally — it doesn't. React's Virtual DOM is a pure JavaScript concept with no browser API dependency.

React uses the key prop to match old and new elements. With keys, it can: reorder elements efficiently, preserve component state during reorder, add/remove specific items without re-rendering the entire list. Always use unique, stable IDs as keys.

Why it matters: Keys let React track which items exist across renders, enabling efficient add, remove, and reorder operations without remounting everything.

Real applications: Drag-and-drop sortable lists, dynamically filtered product catalogs, real-time message feeds.

Common mistakes: Using index as a key for a reorderable list — when items reorder, components get the wrong key and their state gets reassigned to the wrong element.

React's work is split into two phases: render phase (pure, can be interrupted) and commit phase (applies DOM changes, runs effects, cannot be interrupted).

  • Render: calls component functions, diffs virtual DOM — may run multiple times
  • Commit: updates real DOM — runs once per update cycle
  • Effects: useLayoutEffect fires synchronously after commit; useEffect fires asynchronously after paint

Understanding these phases explains why strict mode renders components twice in development — to catch side effects in the render phase.

Why it matters: Knowing the two-phase model helps you place side effects correctly — render phase must be pure, commit phase is where effects belong.

Real applications: Understanding why useEffect and useLayoutEffect fire at different times, why you can't read DOM measurements in the render phase.

Common mistakes: Putting side effects directly in the component function body (render phase) — they can run multiple times causing duplicated API calls or DOM mutations.

React 18's Strict Mode intentionally mounts, unmounts, and remounts every component twice to help detect side effects that depend on mounting happening only once:

// If your effect breaks when run twice, it has a bug:
useEffect(() => {
  // ❌ Problem: incrementing a counter twice
  analytics.pageViews++;
}, []);

// ✅ Fix: cleanup undoes the effect
useEffect(() => {
  const sub = subscribe(channel);
  return () => sub.unsubscribe(); // cleanup properly
}, []);

This double-render only happens in development mode. Production builds mount components once as expected. Fix your effects to be idempotent — running them twice should produce the same result as once.

Why it matters: The double-render helps you catch effects with missing cleanup or side effects in the render phase during development.

Real applications: Any app using React 18 in development — Strict Mode is enabled by default in Create React App and Vite.

Common mistakes: Disabling Strict Mode to hide the double-invoke warning instead of fixing the underlying problem in the effect.

React considers two components the same if they have the same component type (same function or class reference) at the same position in the tree. If the type changes, React unmounts and remounts:

// Same type at same position — React UPDATES (keeps state)
{isAdmin ? <UserProfile role="admin" /> : <UserProfile role="user" />}

// Different types — React REMOUNTS (state reset)
{isAdmin ? <AdminDashboard /> : <UserDashboard />}

// ❌ Defining component inside render creates new type each render
function Parent() {
  function Child() { return <p>Hi</p>; } // new type every render!
  return <Child />; // remounts on every Parent render
}

Never define component functions inside another component's render — it causes remounts and breaks state.

Why it matters: React identifies components by their function reference. Redefining a component on each render creates a different reference, causing a full remount every time.

Real applications: Always define components at the module level, not inside other component functions or render methods.

Common mistakes: Defining a child component inside the parent's function body — it looks clean but causes the child to unmount and remount on every parent render.

Tearing occurs in concurrent mode when React reads from an external store at different times during a single render, getting inconsistent values:

// Without protection — two reads might get different values
function Component() {
  const a = externalStore.getValue(); // React may pause here
  // ... time passes, store updates ...
  const b = externalStore.getValue(); // gets NEW value — inconsistent!
}

// Fix: use useSyncExternalStore (React 18+)
const value = useSyncExternalStore(
  externalStore.subscribe,  // subscribe function
  externalStore.getValue,   // get current value
  externalStore.getServerValue // optional SSR snapshot
);

useSyncExternalStore is for library authors integrating non-React stores (like Redux). It guarantees React reads a consistent snapshot during each render.

Why it matters: Tearing can cause different parts of the UI to show inconsistent data during a concurrent render — a user sees two versions of the same value at once.

Real applications: Integration between React 18 concurrent mode and external state libraries like Redux, Zustand, or MobX.

Common mistakes: Reading from an external mutable store directly without useSyncExternalStore in concurrent mode — may produce visual inconsistencies during transitions.