fw-03 — Analysis

Required invariants

  1. An effect re-runs only if a value it actually read changed. This is the whole value proposition; failing it means you built "re-run everything."
  2. Dependencies are cleared before each re-run. Otherwise a branch no longer read keeps firing — a dependency leak.
  3. The active-effect context is a stack, not a global. A nested effect must not steal the outer effect's dependencies.
  4. computed is lazy and cached, and is simultaneously a dependent and a dependency.
  5. Effects are batched and deduplicated, flushed on a microtask.

The glitch, and why laziness fixes it

const a = signal(1);
const b = computed(() => a.value + 1);
const c = computed(() => a.value + b.value);

Set a = 2. A naive push order can evaluate c after a updated but before b did, computing 2 + 2 = 4 instead of 5. That transient wrong value is a glitch.

Fixes: evaluate in topological order, or make computed lazy so c pulls b. Laziness is cheaper, which is why computed is specified lazy rather than eager.

Test for it. Most hand-written reactive systems have this bug and never notice, because the glitch is transient and the final value is correct.

What tracking cannot see

CaseWhyMitigation
Destructuringreads once, then you hold a plain valuetoRefs-style wrappers
Collectionsmethods must be instrumented, not just property accesswrap Map/Set/array methods
Reads after awaitoutside the tracking contextre-establish, or forbid
Conditional readsstale deps accumulatecleanup before re-run

The trade against explicit models

Automatic tracking removes an authoring burden and adds an observability burden. With Redux you print the action log. With a reactive system, "why did this effect run?" needs a devtool that shows the graph — which is why every mature reactive framework ships one.