Identity

The whole model rests on re-running the program and matching this run's output to the last one. For that to work, every piece of state needs a stable identity: a way to say “this State is the same one I saw last time,” so its value carries across. That identity is a path through a scope tree, and the tree branches at every If and Each.

Why counting calls isn't enough

The obvious scheme is to number the identity-bearing calls in the order they happen: the first State is 0, the next 1, and so on. That works only while the same calls happen in the same order every run. The moment structure can change, it breaks:

const a = Reactive.State(0);   // 0
Reactive.If(showExtra, () => {
  const extra = Reactive.State(""); // 1  … only on runs where showExtra is true
});
const b = Reactive.State(0);   // 2 on some runs, 1 on others — b's identity shifts!
How a flat counter loses identity when a branch appears

When showExtra flips, the flat counter re-numbers everything after the If, so b is mistaken for extra and loses its value. Once branches come and go, position in a flat sequence stops being a stable identity.

The scope tree

A run walks a tree of scopes. Each scope hands out positions to the identity-bearing calls inside it: State, and the junctions If/Each themselves. A junction opens a named child scope rather than adding to its parent's sequence, and everything inside is numbered relative to that child. A state's identity is the path from the root down to it:

Program                            // scope: root
  const a = Reactive.State(0)           // → root/0
  Reactive.If(open, () => {              // junction at root/1 → child scope "root/1/if"
    const extra = Reactive.State("")     // → root/1/if/0
  })
  Reactive.Each(items, (item) => {      // junction at root/2 → one child per key
    const n = Reactive.State(0)         // → root/2/each:{item.key}/0
  })
  const b = Reactive.State(0)           // → root/3   (always — the If never touched this counter)
State identities as paths through the scope tree

Now b is root/3 whether or not the If ran, because the If only ever consumed its own position (root/1); the state inside it lives one level down, in a scope of its own.

How each junction branches

If

Keyed by position

An If opens a child scope keyed by its own place in the parent (so two different Ifs never collide). While the condition holds, state inside is numbered in that child. When it goes false the child stays unentered and its state is dropped; when it returns, the child is re-entered and its state starts fresh: exactly the right lifecycle.

Each

Keyed by element

An Each opens one child scope per element, keyed by the element's stable key rather than its index. So a row's state travels with the row: reorder the list and each row keeps its state; insert or remove and only the added or removed keys create or drop a scope, leaving every other row untouched.

What the path buys

Because identity is a path rather than a position, state is isolated by branch: adding or removing something in one part of the tree leaves the identity of everything in another part intact. Toggling an If preserves the surrounding state and cleanly creates or discards only what's inside it. Reordering a keyed list moves rows while their state stays put. And when the driver reconciles, it matches old to new by these same paths, so it patches in place rather than rebuilding.

This is also what makes re-running the whole program cheap and correct rather than destructive: nothing is recreated just because its neighbours changed.

Status

Built. The scope tree described here is what the runtime uses: State and every emitted node take an id from the current scope's path and counter, and If/Each open child scopes at their junctions, Each keyed by the element's stable key. The Tasks example exercises it directly: rows keep their DOM across adds and removals. Still on the design list: the full discovery pass that works out exactly which named contexts a locked If/Each branch would reach. Today a locked branch surfaces its lock to the nearest Loading boundary directly.