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.


0. Orientation

The 60-second version

"Declarative UI" is a slogan until you have implemented it. This module turns it into a specific algorithm with a specific cost, by building the smallest thing that deserves the name React: elements, a renderer, reconciliation, keys, function components, state, effects.

The bet at the centre of the design, stated as an inequality rather than a slogan:

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

That is a bet, not a theorem. It holds when your alternative is "re-render this region from a template string." It fails when your alternative is "I know exactly which text node changed, so I will set it directly" — which is precisely what fw-03 builds, and why that module exists.

The four things you will discover by hitting them

This module is built around bugs you must produce before you are told the fix, because each one is a design decision that is invisible until you have felt the problem:

You buildYou hitWhich explains
Unkeyed childrenState follows position, not identity, on reorderWhy key exists and cannot be inferred
key={index}The identical bug returnsWhy index keys are "no key with extra steps"
Positional hook storageConditional calls shift every subsequent slotWhy the rules of hooks exist
An effect with [] depsA closure holding render 1's values foreverWhy every value in a component body is a snapshot of one render

Do not skip the broken versions. Keys are meaningless until you have watched typed text follow the wrong row.

The pattern to start collecting

key is the runtime admitting it cannot derive a fact it needs, so it asks the author to promise it. You will meet the same shape four more times in this track — {passive:true} (bi-10), contain/content-visibility (bi-07), sideEffects (fw-07), aria-setsize (fw-10).

When you design a system that must be conservative because it cannot know something, the question is not "how do I analyse harder?" — it is "what is the smallest promise the caller could make that would unblock me?"

What you should be able to do after this module

CapabilityEvidence you have it
Explain the virtual DOM honestlyState it as a cost ratio, and name the workload where it loses
Explain keys from first principlesGive the information-theoretic argument, not the docs' version
Explain the rules of hooksDerive them from your own slot list, and price the alternatives
Debug stale closuresReach for "this value is a snapshot of one render," not "add the dependency"

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

Core terminology

TermDefinition
ElementA plain object {type, props, children} describing intended UI — not a DOM node
ComponentA function from props to an element tree
ReconciliationComparing old and new element trees to compute minimal DOM operations
Virtual DOMThe element tree kept in memory so a diff is possible at all
CommitApplying computed DOM operations; must be atomic to avoid a half-updated UI
KeyAuthor-supplied identity for a child, because the runtime cannot infer it
Positional storageHook state stored in a per-instance list indexed by call order
Rules of hooksThe constraint positional storage forces: same calls, same order, every render
DispatcherThe indirection letting useState mean different things during render vs update
Stale closureA callback holding values captured from an earlier render
Functional updatersetCount(c => c + 1) — avoids depending on the captured value
Dependency arrayManual declaration of what an effect reads; a correctness burden by design
CleanupThe function an effect returns; runs before re-run and on unmount
BailoutSkipping a subtree when nothing relevant changed
Reference identityWhy useCallback/useMemo exist: props compared by ===

Mental models

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

Identity must be supplied because it cannot be derived. Given [A,B,C] → [B,C], did you delete A, or rename A→B and B→C and delete the third? Both are consistent with the data, and they imply completely different DOM operations and completely different component-state outcomes. This is information-theoretic, not an implementation limitation.

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

The rules of hooks are the price of composability, not an oversight. Positional storage is what makes useCustomThing() compose with zero ceremony: a custom hook is just a function calling other hooks, and nothing needs to know its name. Price the alternatives before complaining:

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

The last row is where React eventually went with its compiler — fw-06's subject. Move the analysis to build time and you can relax the runtime constraint.

A virtual DOM buys a simple mental model at the cost of per-update work proportional to the described tree. Right when developer velocity and composability matter more than update cost; wrong when the update rate is high and the tree is large.


Common misconceptions

  1. "The virtual DOM is fast." It is not a speed claim. It is the inequality in §0 — cheaper than the naive alternative, and more expensive than a system that tracked what changed (fw-03). Measure it in your own implementation: changing one item in a 200-item list must cost ≤5 DOM operations. Then count what innerHTML replacement costs.

  2. "React is slow because of the virtual DOM." The accurate version is that React does work proportional to the rendered tree on every update, and relies on bailouts and memoisation to prune it. That is a different claim with a different fix.

  3. "Keys are a performance hint." They are a correctness mechanism. Without them, component state attaches to position, so typed text follows the wrong row. The performance benefit is secondary.

  4. "Index keys are fine for static lists." True, and then the list stops being static. The cost of a real key is usually zero; the cost of discovering the bug in production is not.

  5. "The rules of hooks are an arbitrary restriction." They fall directly out of a list indexed by call order. You will explain this better from your own slot list than the docs do.

  6. "Add the missing dependency and the effect is correct." Sometimes; sometimes you get an infinite loop instead. The tension between under- and over-specified dependencies is real, and the functional updater usually dissolves it by removing the dependency entirely.

  7. "setState is asynchronous because of the network or some queue." It is asynchronous because rendering is scheduled rather than immediate, which is what makes batching possible. fw-04 derives this.

  8. "Missing an effect cleanup is a small leak." Across remounts it is unbounded, and it is exactly fe-02's listener shape. Find one in a heap snapshot before deciding it is minor.


Interview talking points

  • "The virtual DOM's value proposition is a cost inequality, not a speed claim: building a description plus diffing plus minimal mutation has to beat what a naive implementation would mutate. That holds against template-string re-rendering and loses against a system that tracked what changed — which is exactly why signals exist."
  • "Keys can't be inferred, and the argument is information-theoretic. Given [A,B,C] becoming [B,C], deleting A and renaming A→B, B→C are both consistent with the data. So identity has to be supplied. key={index} isn't a weak key — it restores exactly the positional correspondence keys were invented to replace."
  • "The rules of hooks are the price of composability. Positional storage is what lets a custom hook be an ordinary function that calls other hooks with no naming ceremony. The alternatives — named keys, class fields, compiler-assigned slots — each cost something, and React eventually went for the fourth with its compiler."
  • "For useEffect I teach one sentence: every value in a component body is a snapshot of one render. That resolves stale closures better than 'add the dependency,' and it points at the functional updater as the fix that removes the dependency entirely."
  • "I collect a pattern across this whole track: the runtime can't derive something, so the platform lets the author promise it. key, {passive: true}, contain, sideEffects, aria-setsize. When I design a conservative tool, I ask what the smallest promise a caller could make would be, rather than how to analyse harder."

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.


Connections to other modules

  • fw-01 — prerequisite, and the contrast: explicit action-driven change versus re-run-and-diff.
  • fw-03 — the opposite design. Signals know what changed because they tracked the read; this module recomputes and compares. §6.8 there is the comparison you are building toward.
  • fw-04 — stages 8–11 (batching, scheduling, interruptible work) live there deliberately. Deriving Fiber before you have felt the frame budget produces transcription, not understanding.
  • fw-05 — the same diff problem with reactive scoping, plus the keyed-diff algorithms (two-ended, LIS) your naive version motivates.
  • fw-06 — compiler-assigned hook slots and patch flags: moving analysis to build time to relax runtime constraints.
  • fw-11 — read React's element creation, reconciler, hooks storage and DOM renderer against what you built here.
  • bi-04 — the DOM cost model that makes the virtual-DOM bet worth taking at all.
  • fe-01 — why setState batching needs a task boundary rather than a microtask one.
  • fe-02 — missing effect cleanup is the listener leak shape, in framework clothing.