Suggest an editImprove this articleRefine the answer for “Virtual DOM in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Virtual DOM** is React's in-memory JavaScript representation of the real DOM used to calculate minimal UI updates. ```jsx setCount(count + 1); // React builds new Virtual DOM, diffs it, patches only the count text node ``` **Key point:** React diffs two JavaScript trees and applies only the delta to the real DOM, skipping browser reflows for unchanged nodes.Shown above the full answer for quick recall.Answer (EN)Image**Virtual DOM** - React's in-memory JavaScript representation of the real DOM that enables efficient UI updates through diffing and minimal real DOM mutations. ## Theory ### TL;DR - Analogy: sketching changes on paper (Virtual DOM) before painting a wall (real DOM) - only apply the exact strokes needed - Main difference: direct DOM calls like `document.getElementById` trigger synchronous browser reflows; Virtual DOM batches changes and diffs first - Decision rule: frequent state updates → Virtual DOM; static pages with no interactivity → skip React entirely - Keys in lists let React reuse existing DOM nodes on reorder instead of remounting everything - React 18 fiber reconciler makes rendering interruptible, so user input stays responsive during heavy updates ### Quick Example ```jsx // Counter: React diffs old and new Virtual DOM, updates only the text node import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <h1>Count: {count}</h1> {/* Only this text node changes */} <button onClick={() => setCount(count + 1)}>+1</button> </div> ); } // On click: React creates new Virtual DOM tree, diffs it against previous, // finds only <h1> text changed, patches that single text node in real DOM. // The <button> and <div> are untouched. ``` On every click, React builds a new JavaScript object tree representing the UI, compares it to the last one, and sends only the delta to the browser. The `<button>` DOM node is never touched. ### How the Diffing Algorithm Works React's diff algorithm runs in O(n) time using two heuristics. First, if two elements have different types (say, `<div>` becomes `<span>`), React tears down the entire subtree and builds fresh. No attempt to reconcile. Second, when rendering lists, `key` props let React track which items moved, which got added, and which were removed. Without `key`, React falls back to index-based comparison and remounts everything on reorder. The reconciler (the `react-reconciler` package) builds a fiber tree: a linked-list structure where each node represents a component. In React 18, this work is interruptible. High-priority updates like user input can preempt a slow re-render of a data table mid-way through. ### Update Flow 1. **Trigger** - `setState` or `useState` fires, React schedules a re-render 2. **Render phase** - React calls your components and builds a new Virtual DOM tree in memory (plain `React.createElement` calls returning JS objects) 3. **Commit phase** - React diffs the new tree against the previous fiber tree, then applies the minimal set of DOM mutations via `commitRoot` Batching matters here. React groups multiple state updates into one commit instead of flushing after each call. In React 18, batching happens automatically even inside `setTimeout` or native event handlers. ### When to Use - Frequent state updates (user input, real-time data) - Virtual DOM handles batching automatically - Large dynamic lists - pair with `key` props, add `react-window` beyond 500 items - Performance tuning - add `React.memo` or `useMemo` on top of Virtual DOM diffing - Static HTML pages with no interactivity - skip React, direct DOM or plain HTML is faster here ### Common Mistakes **Using array index as `key`** ```jsx // Wrong: inserting at start shifts all indexes, React remounts everything items.map((item, i) => <li key={i}>{item.text}</li>) // Right: stable unique ID items.map(item => <li key={item.id}>{item.text}</li>) ``` Index keys work fine for static, never-reordered lists. The moment you sort, filter, or insert at the top, all keys shift and React remounts every node - losing local state, focus, and running animations. **Assuming Virtual DOM is always faster than direct DOM** For a single toggle or a static text swap, `document.getElementById` is faster. React's overhead comes from the diff itself. SyntheticJS benchmarks show React about 4x slower than direct DOM for trivial single-element updates, and 10x faster for complex lists where batching prevents hundreds of reflows. **Mutating state directly** ```jsx // Wrong: Virtual DOM diffing relies on reference equality state.items.push(newItem); setState(state); // React sees same reference, may skip re-render // Right: new reference setState({ ...state, items: [...state.items, newItem] }); ``` **Deep nested trees without virtualization** 10,000 rows rendered at once will stress the diff regardless of Virtual DOM. Use `react-window` or `react-virtual` to render only visible rows. **Re-renders cascading from parent** If a parent re-renders, all children re-render by default unless wrapped in `React.memo`. Virtual DOM makes this cheap, but not free. The Profiler in React DevTools shows where the cost actually lands. ### Real-world Usage - React core - every component renders to Virtual DOM objects via `jsx-runtime` before any browser paint - Next.js - server renders a Virtual DOM tree, hydrates it on the client by matching against the live DOM - Preact - 3kb Virtual DOM clone used in Hulu's dashboard and 100k+ sites where bundle size matters - Redux DevTools - serializes Virtual DOM snapshots for time-travel debugging - React Native - same diffing mechanism, different commit target (native views instead of browser DOM) ### Follow-up Questions **Q:** How does React decide to replace a subtree vs update it? **A:** Element type comparison. If `<Input>` becomes `<Select>`, React unmounts `Input` and mounts `Select` fresh. If the type stays the same, React patches only the changed props. **Q:** What happens with duplicate or missing keys? **A:** Missing keys fall back to index with a console warning. Duplicate keys cause wrong diffing - React picks one arbitrarily and may produce incorrect UI without throwing an error. **Q:** How does React 18 concurrent mode change the Virtual DOM cycle? **A:** `startTransition` marks updates as low priority. Fiber can suspend work on the new Virtual tree mid-way, let a higher-priority update (like a keystroke) commit first, then resume or discard the interrupted work. The Virtual DOM tree structure is the same, but the scheduler controls when it commits. **Q:** Why not write manual diff logic and skip React? **A:** You can. But React's key/type heuristics cover the common 99% of cases without extra code. Manual diffing becomes maintenance overhead fast, especially once lists, animations, and concurrent updates enter the picture. **Q:** When would you reach for `useMemo` on top of Virtual DOM? **A:** When a component re-renders frequently and one of its children does expensive calculation or renders a large subtree. `useMemo` returns a cached reference so the diff skips that subtree entirely. ## Examples ### Basic: Counter showing minimal DOM updates ```jsx import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <h1>Count: {count}</h1> <button onClick={() => setCount(count + 1)}>+1</button> </div> ); } ``` Click the button three times. React creates a new Virtual DOM tree on each click, diffs it against the previous, and patches only the text node inside `<h1>`. The `<div>` and `<button>` nodes in the real DOM are never touched. Open React DevTools Profiler to see exactly which component committed and how long it took. ### Intermediate: Todo list with keys ```jsx import { useState } from 'react'; const initial = [ { id: 1, text: 'Learn React', done: false }, { id: 2, text: 'Build app', done: true }, ]; function TodoList() { const [todos] = useState(initial); const [filter, setFilter] = useState('all'); const visible = todos.filter(t => filter === 'all' ? true : t.done === (filter === 'done') ); return ( <> <button onClick={() => setFilter('all')}>All</button> <button onClick={() => setFilter('done')}>Done</button> <ul> {visible.map(todo => ( <li key={todo.id}>{todo.text}</li> // stable key = reuse DOM node on filter change ))} </ul> </> ); } ``` Switch filters and React diffs the list, reuses unchanged `<li>` nodes, and adds or removes only what changed. Swap `key={todo.id}` for `key={index}` and re-check the Profiler - every `<li>` remounts on each filter switch. ### Advanced: Index keys vs stable keys under drag-reorder ```jsx // Bad: index keys cause full remounts when list reorders function BadList({ items }) { return ( <ul> {items.map((item, index) => ( <Item key={index} data={item} /> // drag to reorder → 100 unmounts + 100 mounts ))} </ul> ); } // Good: stable ID keys let React swap positions cheaply function GoodList({ items }) { return ( <ul> {items.map(item => ( <Item key={item.id} data={item} /> // drag to reorder → React swaps fiber nodes, preserves state ))} </ul> ); } ``` With 100 draggable items, `BadList` triggers 100 unmounts and 100 mounts on every reorder because every index shifts. `GoodList` only swaps fiber node positions. In React Native's `FlatList`, this difference shows up as visible flicker and lost scroll position on Android. I've seen this mistake in senior developers' PRs more often than you'd expect.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.