Beyond Code & Karma · React Internals

Reconciliation

You never tell React how to change the DOM — you declare the tree you intend, and React reconciles it with what's already mounted, committing only the necessary operations. That is the whole trick behind the virtual DOM, and the single reason key exists.

Interactive Deep Dive · Virtual DOM Diffing

Don't rebuild the world.
Just commit the difference.

When a component's state or props change, React re-runs it and gets a fresh tree of elements — a lightweight, in-memory description of the UI, the virtual DOM. The real browser DOM is slow to mutate, so React never applies the new tree blindly. It diffs the new tree against the previous one, computes the smallest set of real DOM edits, and commits only those. That diff-and-commit cycle is reconciliation.

The Secret: a naive tree-to-tree diff is O(n³). React makes it O(n) with two pragmatic heuristics — same type ⇒ reuse the node, and lists need stable keys. Almost every React performance bug is one of those two rules being violated.
Virtual DOM
A cheap, in-memory tree of elements your render returns. Creating and comparing it is fast; mutating the real DOM is what's expensive — so React minimizes that.
The Diff
Walk old and new tree in lockstep. Same type at a slot → reuse and patch props. Different type → remount the subtree.
Keys
A child's true name in a list. Stable keys let React match by identity across reorders instead of by raw position.
Bailout
If a subtree's props and state didn't change, React can skip it entirely — no diff, no commit. Reuse is the default, work is the exception.

Lila — the play of forms, and what endures

Hindu Philosophy Parallel

In Vedic thought Vishnu is the preserver, and the cosmos is his lila — the divine play of endless forms arising and dissolving. The UI is exactly this: a component re-renders without end, a new tree every tick, the play of forms. Yet identity persists through the churn — and the thread that preserves it is the key. Reconciliation is the dharma that reuses the same node rather than destroying and recreating it. An unstable index key is forgetting true identity — needless death and rebirth.

Lila — the re-render
Every render is a new play of forms. The tree is reborn endlessly; that is its nature, not a flaw.
Key — the true name
Vishnu preserves what endures. A stable key is a node's nama — identity that survives across every rebirth of the tree.
Index key — forgetting
A key welded to position forgets identity. The soul is mistaken for the slot — needless remount, state lost to a false death.

"The forms play and dissolve without end; give each its true name, and what is real is preserved across the churn."

The two heuristics that make it O(n)

React never proves it found the minimal edit script — that would be cubic. It makes two assumptions that hold in practice and collapse the cost to linear.

heuristics.jsx
// Heuristic 1 — same type at the same slot: reuse, patch props.
<div className="card">   →   <div className="card hot">
// REUSE the DOM node, UPDATE one attribute. No remount.

// Heuristic 2 — different type at the same slot: tear down + rebuild.
<ProductCard />        →   <Banner />
// UNMOUNT the old subtree, MOUNT the new one. State is lost.
The element type is the dividing line. Same type at a slot is a cheap in-place update; a changed type throws away the whole subtree and its state. This is why conditionally swapping <Input> for a <Spinner> in the same position remounts — and resets — everything beneath it.

Lists: give each child a stable key

Children are matched in order by default. To match by identity — so reorders, inserts, and deletes are understood correctly — each child needs a stable key drawn from your data, not from its position:

ProductList.jsx
// Stable id as key → identity survives a reorder.
function ProductList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <ProductCard
          key={item.id}      // the node's true name
          name={item.name}
        />
      ))}
    </ul>
  );
}

The trap: index as key

Using the array index as the key is matching-by-position wearing a disguise: the key is the slot. Reorder the list and every index shifts, so React reuses the wrong node for the wrong data — and any focus, selection, or animation stays welded to the slot:

bad-key.jsx
// index as key → the key IS the position.
// Reorder and every index shifts → React reuses the wrong node.
{items.map((item, i) => (
  <ProductCard
    key={i}             // 🚩 forgets identity
    name={item.name}   // data moves, node stays welded to slot i
  />
))}

The three phases — every update is this cycle

Render

State changed → React re-runs the affected components and produces a fresh element tree in memory. No DOM is touched yet. This work is interruptible.

Diff

Walk old and new tree in lockstep. Classify each node: matched, props-updated, newly-mounted, or unmounted. The output is a minimal list of effects.

Commit

Apply the effect list to the real DOM in one synchronous batch — insert, remove, update text and attributes, run layout effects. Short list = fast frame.

Watch a node get reused — or remounted

A small before/after diff, auto-playing. Same type at a slot is reused and patched; a changed type is remounted. Watch the classification change as the new tree mutates.

Old tree → New tree · diff teaser
⏸ Ready
Old tree
    New tree
      Press Run to start…

      The nuances that senior devs know

      This is where the 10,000 basic videos stop. Here's what actually decides whether your UI stutters.

      The diff cheat-sheet — what each case costs

      SituationWhat React doesCost & state
      Same type, same slotReuse the DOM node, patch changed props onlyCheap · state preserved
      Different type, same slotRemount — unmount old subtree, mount newExpensive · state destroyed
      Keyed list, reorderedMove existing nodes to new positionsCheap · state follows the item
      Index-keyed list, reorderedRewrite content in place at each slotWasteful · state welded to slot

      The general problem — find the minimum edit distance between two arbitrary trees — is O(n³). React refuses to pay that. It assumes (1) two elements of different type produce different trees, so it never tries to morph a <div> into a <span> — it replaces; and (2) children keep a stable identity across renders via key. Those two assumptions let it diff in a single linear pass. The cost of the shortcut is precisely the bugs in this chapter: they're what happens when reality violates an assumption.

      Index keys don't usually look broken — the list renders the right text after a reorder. The damage is to anything the DOM node holds that React can't see in props:

      Lost to index keysWhy
      Input focus & cursorFocus lives on the DOM node; reusing it for different data points focus at the wrong row.
      Uncontrolled input textA half-typed value stays in the slot's node and is now shown for a different item.
      CSS transitions in flightThe node isn't moved, just rewritten — so the move never animates.
      Component stateState keyed to the node, not the data, follows the slot, not the item.
      Rule of thumb: key by a stable unique id from your data. Index keys are safe only for a static, append-only list whose items hold no internal state.

      The flip side of "stable keys preserve identity" is that changing a key forces a remount. That's a feature: give a component key={userId} and switching users blows away all internal state and effects, giving you a guaranteed fresh mount instead of a stale, half-updated component. It's the idiomatic way to say "this is a different thing now, start over."

      Reconciliation only runs where it must. If a component's props are referentially equal and its state didn't change, React can bail out and skip its subtree. React.memo, useMemo, and stable callbacks exist to keep those props referentially equal so the bailout fires. The common performance bug isn't a slow diff — it's a new object or inline function passed as a prop every render, defeating the bailout and forcing needless reconciliation down the tree.

      Under the hood the reconciler is Fiber: the render-phase work is broken into units React can pause, resume, and abandon, so a long render doesn't block the main thread. Concurrent features (transitions, Suspense) build on that. But the rules in this chapter are unchanged — same-type reuse, key identity, render then commit. Server Components shift where components render and how much JS ships; on the client the diff-and-commit cycle, and keys, matter exactly as much.

      The Karma of reconciliation

      The Karma Connection

      Declaring the new tree is sankalpa — a stated intention. The reconciler performs nishkama karma: right action without waste, without attachment to re-rendering for its own sake. It does not cling to the old form, and it does not rebuild the world. It acts only where action is required.

      karma.js
      // The reconciler does not cling, and does not destroy in haste.
      diff(oldTree, newTree); // declare intention (sankalpa), see only the delta
      
      // What is unchanged is left untouched — reused, not reborn.
      // What truly differs is committed — nothing more (nishkama karma).
      commit(effects);        // act only where action is required

      What is unchanged is left untouched — reused, not reborn. What truly differs is committed, and nothing more. Give each node its true name — the key — and identity survives the churn of rebirth. The diff is small, the frame is fast, and nothing is destroyed in haste.

      "Act only where action is required; reuse what endures, release only what has truly changed." — the Gita, interpreted for the render loop 🙏

      Reconciliation Trivia

      Three questions. No cheating. Your karma is watching.

      Question 1
      A slot holds <Input /> on one render and <Spinner /> on the next. What does React do?
      Question 2
      You reverse a list rendered with key={index}. The text looks right afterward. What actually happened to the DOM?
      Question 3 — 🌶️ Spicy
      Your child re-renders every time the parent does, even though its data is unchanged. Most likely cause?

      The Six Truths of Reconciliation

      Diff, Don't Rebuild
      React compares the new element tree to the old and commits only the delta. The virtual DOM exists to keep real DOM writes minimal.
      Same Type → Reuse
      A node of the same type at the same slot keeps its DOM node; only changed props are patched. Reuse is the default.
      New Type → Remount
      A changed type at a slot unmounts the old subtree and mounts the new one. State beneath it is destroyed.
      Keys Are Identity
      A stable key matches a child by who it is, not where it sits — so reorders become cheap moves, not in-place rewrites.
      Index Keys Lie
      key={index} is matching by position in disguise. On reorder, state and focus stay welded to the slot, not the item.
      Bail Out Early
      Unchanged subtrees are skipped entirely. Keep props referentially stable and reconciliation does the least work it can.

      कर्म करो, फल की चिंता मत करो