Concepts — mini-react I: Elements, Rendering, Reconciliation, State

Phase 2, parallel with bi-07 · Spec area §27 stages 1–7. Prerequisites: bi-04 (DOM cost model), fw-01.

Stages 8–11 (batching, scheduling, interruptible work) are not here. They are fw-04, after bi-11. Attempting them now produces a transcription of Fiber rather than a derivation of it.


1. Why a Principal Engineer needs this

You cannot evaluate React — or argue about it credibly — from its documentation. The design decisions that matter (why a virtual DOM at all, why keys, why hooks are order-dependent, why setState is asynchronous) are only legible once you have hit the problem each one solves.

The deeper aim is transferable: this module is where "declarative UI" stops being a slogan and becomes a specific algorithm with specific costs.


2. Mental Model

Component functions
   -> elements (plain data: {type, props, children})
   -> reconciliation against the previous tree
   -> a minimal set of DOM operations
   -> commit

The core bet: describing the whole UI and diffing is cheaper than manually tracking what changed — because DOM mutation is expensive (bi-04) but object allocation and comparison are cheap. Whether that bet pays depends entirely on the ratio, which is why it is a bet and not a theorem, and why fw-03's alternative exists.


3. Build order

  1. Element model. createElement(type, props, ...children) returning plain objects. No classes, no magic. Write h('div', {id:'x'}, 'hi') by hand and look at the object.
  2. DOM renderer. Render an element tree to real DOM. No diffing yet — mount only.
  3. Reconciliation. Given old and new trees, compute and apply the minimal DOM changes. Same-type nodes update in place; different types replace.
  4. Keyed children. Implement unkeyed first, find the bug (state attaching to the wrong item on reorder), then add keys. Do not skip the broken version — keys are meaningless until you have seen what they fix.
  5. Function components. Components return element trees; render recursively.
  6. State. useState with a per-component slot list. Discover that this requires stable call order, and write down why the rules of hooks exist before reading that they do.
  7. Effects. useEffect with dependency comparison, cleanup on unmount and on dep change. Get the cleanup ordering wrong at least once.

4. Failure Lab

  1. Unkeyed reorder. A list of inputs with text typed in them, reordered. Watch state follow position instead of identity. This is the canonical demonstration.
  2. Wrong key. Use array index as key on a reorderable list. Show the same bug returns.
  3. Hook order violation. Call a hook conditionally. Explain the failure precisely in terms of your slot list — you will explain it better than the docs do.
  4. Missing dependency. A stale closure reading old state. Then over-specify dependencies and produce an infinite effect loop. Both are real; describe the tension.
  5. Missing cleanup. Subscribe in an effect without unsubscribing; leak across remounts. Find it in a heap snapshot (bi-04's retainer-chain skill).

5. Trade-offs to argue

Diffing vs tracking. React re-renders and diffs; Vue/signals track dependencies. React trades per-update work for a simpler mental model and better composability. Where does the trade stop paying?

Keys as author-supplied identity. The runtime cannot infer identity, so it asks. Compare with {passive:true} in bi-10: an author-supplied guarantee that unlocks an optimisation the runtime could not otherwise make. Once you notice this pattern, you see it everywhere.

Hooks' positional storage. Enables tiny API surface and composition; costs the rules of hooks and a whole class of confusing errors. Design an alternative and cost it out.


5.5 Deep dive: why keys cannot be inferred

The runtime sees two arrays of descriptions. It must decide which old item corresponds to which new one. Without keys the only available correspondence is position.

That is not a limitation of the implementation; it is information-theoretic. Consider:

before: [A, B, C]
after:  [B, C]

Did you delete A, or rename A→B, B→C and delete the third? Both are consistent with the data. The runtime cannot distinguish them, and the two answers imply completely different DOM operations and completely different component-state outcomes.

So identity must be supplied. key is the author asserting "this description refers to the same conceptual thing as the one with the same key last time."

This is the third instance of the same pattern in the track, and it is worth collecting them explicitly: key (fw-02), {passive: true} (bi-10), contain / content-visibility (bi-07), sideEffects (fw-07). In every case the runtime cannot derive a fact it needs, so the platform adds a way for the author to promise it — and an optimisation becomes possible that was previously impossible in principle.

When you design a system that must be conservative because it cannot know something, ask whether the caller could simply tell you.

Why array index is not a key

key={index} restores exactly the positional correspondence keys were meant to replace. It is not "a weak key"; it is no key with extra steps, and it fails identically on reorder, insert-at-front, and delete-from-middle. It is only safe when the list is append-only and never reordered — at which point it is also unnecessary.


5.6 Deep dive: hooks, and the cost of positional storage

useState stores per-component-instance state in a list indexed by call order. That is why the rules of hooks exist: conditional calls shift every subsequent index.

Derive the alternatives and their costs, because "the rules of hooks are annoying" is only a complaint until you have priced the options:

DesignCost
Positional (React)rules of hooks; confusing errors; but tiny API and perfect composability
Named keys (useState('count', 0))no ordering rules; but every hook needs a unique name, and composition requires namespacing
Class fieldsexplicit and safe; but no composition without mixins/HOCs — the problem hooks were created to solve
Compiler-assigned slotsbest of both; requires a build step and makes runtime-only use impossible

Positional storage is what makes useCustomThing() compose with zero ceremony — a custom hook is just a function that calls other hooks, and nothing needs to know its name. The rules of hooks are the price of that, and it is a real trade rather than an oversight.

Note the fourth row is where React eventually went with its compiler, which is fw-06's subject: move the analysis to build time and you can relax the runtime constraint.

The stale-closure problem

useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);  // captures count from THIS render
  return () => clearInterval(id);
}, []);                                                     // never re-runs

The effect closed over the first render's count, forever. The functional updater (setCount(c => c + 1)) fixes it by not depending on the captured value at all.

This is not a React bug — it is JavaScript closure semantics meeting a render model where the function body runs many times. Every value in a component body is a snapshot of one render. Internalising that sentence resolves most useEffect confusion, and it is a much better mental model than "add the dependency."


5.7 Deep dive: what "the virtual DOM is fast" actually claims

The claim is not that diffing is faster than DOM mutation. It is:

cost(build description) + cost(diff) + cost(minimal mutations)
    <  cost(mutations a naive implementation would perform)

That inequality holds when your alternative is "re-render this region from a template string," and fails when your alternative is "I know exactly which text node changed, so I'll set it directly."

Which is precisely why fine-grained reactive systems (fw-03) can win: they do know, because they tracked it.

Your spec's stage-3 test measures the left-hand side directly — changing one item in a 200-item list must cost ≤5 DOM operations. Run the same scenario with innerHTML replacement and count: that is the right-hand side. The virtual DOM's value proposition is a measurement you can make in your own implementation, not a claim to accept or reject on authority.

The honest summary for a design review:

A virtual DOM buys you a simple mental model (describe the whole UI; the runtime works out the difference) at the cost of per-update work proportional to the described tree. It is the right trade when developer velocity and composability matter more than update cost, and the wrong one when the update rate is high and the tree is large.


6. Verification

  • Renders and updates a non-trivial UI
  • Keyed reconciliation demonstrably fixes the reorder bug
  • useState, useEffect with correct cleanup ordering
  • All five failure-lab bugs reproduced and explained
  • Measured: DOM operations per update, mini-react vs naive innerHTML replacement
  • Complexity-notebook entry: why keys must be author-supplied

7. Principal Engineer Review

  1. Explain the virtual DOM's value proposition in terms of cost ratios, and name the workload where it is a net loss.

  2. Why can't the runtime infer list identity without keys? Give the theoretical answer and the practical one.

  3. Hooks depend on call order. Design an alternative with the same composability. What does it cost in API surface or ergonomics?

  4. useEffect dependency arrays are a manual correctness burden. Argue they are essential; then argue the compiler should do it (and note who has tried).

  5. A colleague says "React is slow because of the virtual DOM." Give the accurate version.

  6. You are choosing a rendering model for a 100k-row data grid. What does the diffing model cost you here specifically, and what would you do instead?

  7. Your mini-react re-renders a whole subtree on any state change. Name every mechanism production React uses to avoid that, and what each costs.

  8. What would break if setState were synchronous? Answer before fw-04, then revisit after.