Virtual DOM in React — Complete Guide
In this tutorial, you will learn about Virtual Dom in React. We cover key concepts, practical examples, and best practices to help you master this topic.
Virtual DOM in React uses the Fiber reconciler with automatic batching, hooks-based lifecycle, and concurrent features to deliver high-performance declarative UIs.
What You'll Learn
- How React implements Virtual DOM with Fiber
- How React's reconciliation differs from other frameworks
- How hooks interact with the Virtual DOM
- How to optimize React rendering
Why It Matters
React is the most popular Virtual DOM framework. Understanding its specific implementation helps you write performant React components and debug rendering issues effectively.
flowchart TD
A[React Component] --> B[createElement or JSX]
B --> C[React Element tree]
C --> D[Fiber Reconciler]
D --> E{Concurrent mode?}
E -->|Yes| F[Interruptible render]
E -->|No| G[Synchronous render]
F --> H[Commit phase]
G --> H
H --> I[DOM updates]
React Elements vs Fiber Nodes
React has two representations: elements (from JSX) and fibers (internal work units).
// React Element: what your component returns
// Created by JSX or createElement
const element = (
<div className="container">
<h1>Hello</h1>
<p>World</p>
</div>
);
// The element is a plain object:
// {
// type: 'div',
// props: { className: 'container', children: [...] },
// key: null,
// ref: null,
// $$typeof: Symbol(react.element)
// }
// React Fiber: internal work unit created from elements
// Fibers are persistent and reused across renders
// {
// tag: HostComponent,
// type: 'div',
// stateNode: domNode,
// memoizedProps: { className: 'container' },
// memoizedState: null,
// child: fiberNode,
// sibling: fiberNode,
// return: parentFiber,
// lanes: 0,
// alternate: previousFiber
// }
// React maintains two fiber trees:
// 1. Current tree (rendered on screen)
// 2. Work-in-progress tree (being built)
// The alternate link connects corresponding nodes in both trees
Reconciliation in React
React's reconciliation algorithm walks the fiber tree and compares types.
// Simplified React reconciliation logic
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// Single element
return reconcileSingleElement(returnFiber, currentFirstChild, newChild);
case REACT_PORTAL_TYPE:
return reconcileSinglePortal(returnFiber, currentFirstChild, newChild);
}
}
if (newChild instanceof Array) {
// Array of elements (list)
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text content
return reconcileSingleTextNode(returnFiber, currentFirstChild, newChild);
}
// null, undefined, boolean: delete
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
// Keyed list reconciliation:
// React uses a key-based map for O(n) list diffing
// 1. Build map of existing children by key
// 2. Iterate new children, match by key
// 3. Handle moves, additions, deletions
Hooks and the Virtual DOM
Hooks are stored on the fiber node in a Linked List.
function useState(initialValue) {
// Get the current fiber's hook list
const fiber = getCurrentFiber();
const hook = fiber.memoizedState;
if (hook === null) {
// First render: create hook state
const newHook = {
memoizedState: initialValue,
baseState: initialValue,
baseQueue: null,
queue: null,
next: null
};
// Append to fiber's hook list
fiber.memoizedState = newHook;
// Create dispatch function
const dispatch = dispatchAction.bind(null, fiber, newHook.queue);
return [initialValue, dispatch];
}
// Subsequent render: use existing hook state
return [hook.memoizedState, hook.queue.dispatch];
}
function dispatchAction(fiber, queue, action) {
// Create update object
const update = {
lane: requestUpdateLane(),
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
// Queue the update on the fiber
const last = queue.last;
if (last === null) {
update.next = update;
} else {
// Circular linked list
const first = last.next;
last.next = update;
update.next = first;
}
queue.last = update;
// Schedule a re-render
scheduleUpdateOnFiber(fiber, update.lane);
}
Bailing Out of Re-Renders
React provides several mechanisms to skip re-rendering.
// 1. React.memo: shallow prop comparison
const MemoizedComponent = React.memo(function MyComponent({ data }) {
return <div>{data.value}</div>;
});
// Parent re-renders but data reference hasn't changed:
// MemoizedComponent skips re-rendering
// 2. useMemo: memoize expensive computations
function Expensive({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.type === filter);
}, [items, filter]);
// Only recomputes when items or filter change
return filteredItems.map(item => <li key={item.id}>{item.name}</li>);
}
// 3. useCallback: memoize function references
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// Stable reference across renders
return <Child onClick={handleClick} />;
// Child won't re-render just because handleClick changed
}
// 4. Bailout in reconciliation
// React compares memoizedProps with pendingProps
// If they are equal (shallow), the fiber subtree is skipped
function beginWork(current, workInProgress) {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps === newProps && !workInProgress.lanes) {
// No changes, bail out of this subtree
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
}
// Process this fiber
// ...
}
Concurrent Features
React 18's concurrent features depend on Fiber's interruptible rendering.
import { startTransition, useTransition, useDeferredValue } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
// Urgent update: input should respond immediately
setQuery(e.target.value);
// Transition update: filtering can be deferred
startTransition(() => {
setSearchResults(filterData(e.target.value));
});
}
// useDeferredValue: automatically delay a value
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// While the transition is pending:
// - isPending is true
// - The old search results stay visible
// - The input stays responsive
// - The browser doesn't freeze
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<Results data={filterData(deferredQuery)} />
</div>
);
}
Common Mistakes
- Creating new object/array references in render, causing pure component memoization to fail.
- Using inline arrow functions in JSX without useCallback, breaking child memoization.
- Calling hooks conditionally or in loops, violating the rules of hooks.
- Assuming concurrent features work without the new root API (createRoot).
- Over-optimizing with memo/useMemo before measuring actual performance.
Practice Questions
- How does React represent a component's state internally? As a linked list of hook objects stored on the fiber node.
- What is the difference between a React element and a fiber? Elements are plain objects from JSX. Fibers are internal work units with state, effects, and tree links.
- How does React.memo prevent re-renders? It performs a shallow comparison of props and skips re-rendering if nothing changed.
- What enables React's concurrent features? Fiber's interruptible render phase and lane-based prioritization.
Challenge
Build a React app that demonstrates the difference between urgent and transition updates. Create a search input that filters a large list (10000 items). Use useTransition to keep the input responsive. Show the pending state indicator and measure the input latency with and without transitions.
FAQ
Mini Project
Build a React performance profiler that wraps any component and logs: render count, time spent rendering, number of child re-renders, and whether the component bailed out. Use it to profile a complex form and identify optimization opportunities.
What's Next
Lesson 11: Virtual DOM in Vue
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro