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, afterbi-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 build | You hit | Which explains |
|---|---|---|
| Unkeyed children | State follows position, not identity, on reorder | Why key exists and cannot be inferred |
key={index} | The identical bug returns | Why index keys are "no key with extra steps" |
| Positional hook storage | Conditional calls shift every subsequent slot | Why the rules of hooks exist |
An effect with [] deps | A closure holding render 1's values forever | Why 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
| Capability | Evidence you have it |
|---|---|
| Explain the virtual DOM honestly | State it as a cost ratio, and name the workload where it loses |
| Explain keys from first principles | Give the information-theoretic argument, not the docs' version |
| Explain the rules of hooks | Derive them from your own slot list, and price the alternatives |
| Debug stale closures | Reach 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
- Element model.
createElement(type, props, ...children)returning plain objects. No classes, no magic. Writeh('div', {id:'x'}, 'hi')by hand and look at the object. - DOM renderer. Render an element tree to real DOM. No diffing yet — mount only.
- Reconciliation. Given old and new trees, compute and apply the minimal DOM changes. Same-type nodes update in place; different types replace.
- 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.
- Function components. Components return element trees; render recursively.
- State.
useStatewith 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. - Effects.
useEffectwith dependency comparison, cleanup on unmount and on dep change. Get the cleanup ordering wrong at least once.
4. Failure Lab
- Unkeyed reorder. A list of inputs with text typed in them, reordered. Watch state follow position instead of identity. This is the canonical demonstration.
- Wrong key. Use array index as key on a reorderable list. Show the same bug returns.
- 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.
- Missing dependency. A stale closure reading old state. Then over-specify dependencies and produce an infinite effect loop. Both are real; describe the tension.
- 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:
| Design | Cost |
|---|---|
| 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 fields | explicit and safe; but no composition without mixins/HOCs — the problem hooks were created to solve |
| Compiler-assigned slots | best 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,useEffectwith 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
| Term | Definition |
|---|---|
| Element | A plain object {type, props, children} describing intended UI — not a DOM node |
| Component | A function from props to an element tree |
| Reconciliation | Comparing old and new element trees to compute minimal DOM operations |
| Virtual DOM | The element tree kept in memory so a diff is possible at all |
| Commit | Applying computed DOM operations; must be atomic to avoid a half-updated UI |
| Key | Author-supplied identity for a child, because the runtime cannot infer it |
| Positional storage | Hook state stored in a per-instance list indexed by call order |
| Rules of hooks | The constraint positional storage forces: same calls, same order, every render |
| Dispatcher | The indirection letting useState mean different things during render vs update |
| Stale closure | A callback holding values captured from an earlier render |
| Functional updater | setCount(c => c + 1) — avoids depending on the captured value |
| Dependency array | Manual declaration of what an effect reads; a correctness burden by design |
| Cleanup | The function an effect returns; runs before re-run and on unmount |
| Bailout | Skipping a subtree when nothing relevant changed |
| Reference identity | Why 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:
| Design | Cost |
|---|---|
| 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 fields | explicit and safe; no composition without mixins/HOCs — the problem hooks solved |
| Compiler-assigned slots | best 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
-
"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 whatinnerHTMLreplacement costs. -
"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.
-
"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.
-
"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.
-
"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.
-
"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.
-
"
setStateis 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-04derives this. -
"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
useEffectI 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
-
Explain the virtual DOM's value proposition in terms of cost ratios, and name the workload where it is a net loss.
-
Why can't the runtime infer list identity without keys? Give the theoretical answer and the practical one.
-
Hooks depend on call order. Design an alternative with the same composability. What does it cost in API surface or ergonomics?
-
useEffectdependency arrays are a manual correctness burden. Argue they are essential; then argue the compiler should do it (and note who has tried). -
A colleague says "React is slow because of the virtual DOM." Give the accurate version.
-
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?
-
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.
-
What would break if
setStatewere synchronous? Answer beforefw-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— whysetStatebatching needs a task boundary rather than a microtask one.fe-02— missing effect cleanup is the listener leak shape, in framework clothing.