Reactive Dependency Graph and State Derivation
Practical 07 - Reactive Dependency Graph and State Derivation
Related: Chapter 7 · Lecture slides
Objective
Build a transparent, educational reactive dependency graph in TypeScript from scratch. You will implement the core primitives that power modern reactive architectures:
- Signals (State Sources): Observable containers that register subscribers on read and notify on write.
- Computed Values (Pure Derivations): Lazy, cached derivations that re-evaluate only when an upstream dependency changes.
- Effects (Synchronization Boundaries): Side-effect observers that react to dependency invalidations and execute explicit cleanup callbacks.
You will trace exact execution timelines, observe dynamic dependency switching, verify cache reuse, and demonstrate how cycles and infinite loops occur when side effects mutate source state.
Educational Model Notice: This laboratory constructs a pedagogical reactive runtime (~100 lines of code) to make dependency discovery and caching mechanics visible. To keep concepts accessible, it deliberately omits advanced production features such as topological glitch-free resolution (diamond dependency sorting), weak reference garbage collection, multi-priority concurrent scheduling, and compiler-level AST transforms.
Prerequisites and Workspace Setup
You need Node.js (v18+) and the TypeScript compiler.
Initialize your workspace:
Ensure your tsconfig.json enforces strict mode:
Stage 1 - The Signal Primitive and Subscriber Context
Reactivity requires discovering which computations depend on which data. In src/reactive.ts, implement a global subscriber stack and the createSignal primitive:
Stage 2 - Lazy Computed Values and Invalidation
A computed value represents derived data. It must never perform eager calculation if nobody is reading it, and it must never recompute if its upstream dependencies have not changed.
Implement createComputed:
Verify that calling get() three times without modifying source signals invokes fn() exactly once.
Stage 3 - Effects and Resource Cleanup
An effect bridges pure reactive state to imperative external systems (DOM rendering, network dispatch, storage persistence).
Implement createEffect with explicit cleanup:
Stage 4 - Verification Matrix and Cycle Analysis
1. Cycle Hazard Experiment
In src/main.ts, deliberately construct a cyclic dependency:
Observe the resulting browser crash / stack overflow (Maximum call stack size exceeded). Explain why production systems (like Vue’s queue watcher and React’s loop detection) enforce maximum update depth thresholds (e.g., 50 or 100 iterations) and throw explicit architectural errors.
2. Verification Matrix
| # | Action | Expected Observable Result | Status |
|---|---|---|---|
| V1 | Read a computed value 5 times sequentially without signal mutation | Underlying calculation function logs execution exactly once (cached). | |
| V2 | Mutate unrelated signal | Computed function is not re-evaluated. | |
| V3 | Mutate source dependency of an effect | Previous cleanup callback executes before the new effect body runs. | |
| V4 | Conditional branch: computed(() => useA() ? sigA() : sigB()) | When useA switches to false, mutations to sigA no longer trigger recalculation. | |
| V5 | Dispose effect using teardown handle | Future signal changes do not trigger the disposed effect. |
Evaluation Rubric
| Criterion | Exemplary (4) | Proficient (3) | Developing (2) | Inadequate (1) |
|---|---|---|---|---|
| Reactivity Architecture | Clean implementation of subscriber stack, signal access, and notification decoupling. | Core reactivity works, but leaks subscribers across re-runs. | Manual subscription passing; lacks automatic dependency discovery. | Broken reactivity requiring explicit manual trigger calls. |
| Derived State & Caching | Computed values are strictly lazy; cached values served on repeated reads; invalidated only when dirty. | Computed values work, but calculate eagerly upon dependency change. | Calculates on every read; lacks caching. | Computed values fail to update when sources change. |
| Effect & Cleanup Lifecycle | Robust cleanup lifecycle (onCleanup runs before re-run and on disposal); prevents memory leaks. | Effect re-runs correctly, but cleanup is executed at the wrong phase. | Effects run, but lack any cleanup mechanism. | Effects cause uncaught infinite recursion on simple updates. |
| Architectural Boundaries | Clear distinction between pure derivation and imperative effects; models cycle detection and limitations. | Explains limitations, but misses the distinction between derivation and effects. | Confuses state derivation with effects. | No understanding of why cyclic mutations break reactive graphs. |