Skip to content
React rc rendering 5 min read

Reconciliation & the Virtual DOM

When state changes, React does not rebuild the page from scratch. Instead it builds a fresh in-memory description of what the UI should look like, compares it to the previous description, and applies the smallest set of real DOM operations needed to make them match. That comparison process is called reconciliation, the in-memory description is the virtual DOM, and understanding both is what lets you reason about why a component re-renders, why state resets, and why keys matter so much.

The virtual DOM

Every time you call a component, the JSX it returns evaluates to plain JavaScript objects—lightweight descriptions of elements, not actual DOM nodes. This tree of objects is the virtual DOM. Creating and discarding these objects is cheap; touching the real DOM (layout, paint, reflow) is expensive. React leans on that asymmetry: it does the heavy thinking against the cheap representation, then commits only the differences to the browser.

const element = <button className="primary">Save</button>;

// is roughly equivalent to:
const element = {
  type: "button",
  props: { className: "primary", children: "Save" },
};

On each render React produces a new tree of these objects and hands it to the reconciler to compare against the tree from the previous render.

How diffing works

Comparing two arbitrary trees is an O(n³) problem in the general case. React makes it O(n) with two pragmatic heuristics that hold for almost all real UIs.

        Previous tree                 Next tree
            <div>                        <div>
           /     \                      /     \
      <Header/>  <List>            <Header/>  <List>
                  |                            |
               <li>×3                       <li>×4   ← one item added

   React walks both trees in lockstep, level by level,
   comparing nodes at the same position.

Same position, same type — update in place. If an element has the same type as the element in the same slot of the old tree (both <div>, or both the same component function), React keeps the existing DOM node and component instance, and only patches the props that changed.

Same position, different type — tear down and rebuild. If the type differs (<div> became <span>, or <Profile> became <Settings>), React assumes the subtrees are unrelated. It unmounts the old node and its entire subtree—destroying state and running cleanup—then mounts the new one fresh.

function Panel({ loggedIn }) {
  // Switching the OUTER type from <p> to <section> would unmount the
  // input and lose its state. Keeping the type stable preserves it.
  return loggedIn ? (
    <section>
      <input placeholder="Search" />
    </section>
  ) : (
    <section>
      <p>Please log in to search.</p>
    </section>
  );
}

Why type identity matters

Because the diff keys off type, where a component sits in the tree determines whether its state survives. Two components of the same type in the same position are treated as the same instance across renders, even if their props change completely. Conversely, rendering the “same” component at a different position—or under a different parent type—gives it a brand-new identity and resets its state and effects.

function Layout({ wide }) {
  // Bug: defining a component inside render creates a NEW function
  // identity every render, so React sees a different `type` each time
  // and remounts it, blowing away its state on every parent render.
  function Counter() {
    const [n, setN] = useState(0);
    return <button onClick={() => setN(n + 1)}>{n}</button>;
  }
  return wide ? <Counter /> : <Counter />;
}

Never declare a component inside another component’s body. The inner function is a fresh value on every render, so React reconciles it as a different type and remounts it—silently resetting state and re-running effects. Hoist it to module scope.

Keys for lists

Within a list of siblings of the same type, position alone is ambiguous: did item 2 get edited, or was a new item inserted at the top pushing everything down? Keys resolve that ambiguity. React matches old and new children by key rather than by index, so it can move, insert, and delete the right nodes while preserving each item’s DOM and state.

// With stable keys, reordering moves nodes; state stays attached
// to the correct item. With index keys, React thinks position 0 is
// "the same item" and mismatches DOM, focus, and form values.
{users.map((user) => (
  <UserRow key={user.id} user={user} />
))}
ScenarioWithout stable keysWith stable keys
Append to endWorks by luckCorrect
Insert at topWrong rows update; state shiftsCorrect, one node inserted
ReorderState/focus jumps to wrong itemState follows its item
Remove middleTrailing items get wrong stateCorrect node removed

The Fiber reconciler

Since React 16, reconciliation runs on an architecture called Fiber. A fiber is a unit of work—one node in the tree—holding its type, props, state, and pointers to its parent, child, and sibling. The key win is that Fiber makes reconciliation interruptible. Rather than diffing the whole tree in one uninterruptible recursive pass, React works through fibers in small chunks. It can pause, yield to the browser to handle a click or paint a frame, and resume later.

This is what powers concurrent features. React can prioritize urgent updates (a keystroke) over non-urgent ones (a startTransition-wrapped filter), and even abandon in-progress work if newer state arrives. The work splits into two phases: the render phase (build and diff fibers; pure, interruptible, no DOM mutations) and the commit phase (apply the computed changes to the DOM; synchronous and uninterruptible). Because the render phase can run more than once or be thrown away, render logic must stay pure—a rule Strict Mode helps enforce by intentionally double-invoking components in development.

Best Practices

  • Keep an element’s type stable across renders when you want to preserve its DOM node and state; change the type only when you genuinely want a remount.
  • Define components at module scope, never inside another component’s body, to avoid accidental remounts.
  • Give list items stable, unique keys derived from data identity—not array indices—so state follows the correct item.
  • Treat render functions as pure: no side effects, no mutation; let effects run after commit.
  • Use a key deliberately as a remount signal when you want to reset a subtree’s state (e.g. key={userId} on a form).
  • Trust the diff—avoid manual DOM manipulation that fights React’s commit, which leads to mismatches.
Last updated June 14, 2026
Was this helpful?