Skip to content

Virtual DOM in React — Complete Guide

DodaTech Updated 2026-06-28 6 min read

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

  1. Creating new object/array references in render, causing pure component memoization to fail.
  2. Using inline arrow functions in JSX without useCallback, breaking child memoization.
  3. Calling hooks conditionally or in loops, violating the rules of hooks.
  4. Assuming concurrent features work without the new root API (createRoot).
  5. Over-optimizing with memo/useMemo before measuring actual performance.

Practice Questions

  1. How does React represent a component's state internally? As a linked list of hook objects stored on the fiber node.
  2. 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.
  3. How does React.memo prevent re-renders? It performs a shallow comparison of props and skips re-rendering if nothing changed.
  4. 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

How does React implement the Virtual DOM?

React uses the Fiber reconciler, which maintains a linked list of fiber nodes representing components and DOM elements.

What is the difference between React elements and fibers?

React elements are immutable description objects from JSX. Fibers are mutable work units that hold state, effects, and can be reused across renders.

How do hooks interact with the Virtual DOM?

Hooks store their state on the fiber node in a linked list. Each render reads the current hook state from the fiber.

What is React.memo and how does it work?

React.memo is a higher-order component that performs shallow prop comparison. If props haven't changed, it skips re-rendering the component.

How does concurrent mode in React 18 work?

Concurrent mode uses Fiber's interruptible rendering. Lower priority updates (transitions) can be paused by higher priority updates (input events).

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