Effects and Computed
Phase 1 left one thing manual: subscribe(cart, 'items', fn) - you had to say which properties
a function cares about. Real frameworks never ask. A Vue template, a Svelte $effect, an Angular
computed - they all figure out their own dependencies. The mechanism behind all of them fits in
one sentence, and it's the best trick in frontend engineering:
💡 Key point: keep a global variable pointing at the function currently running. While it
runs, every reactive property it reads sees that global in the get trap - and subscribes it.
Reading is subscribing.
effect(): the trick, implemented
// --- the machinery from phase 1, plus the one new variable ---
const ledger = ;
let activeEffect = null; // ← the whole trick
// --- watch it work ---
const user = ;
;
user. = 'Grace'; // effect re-runs
user. = 'Hopper'; // effect re-runs
user. = 41; // effect does NOT re-run - it never read visits
What just happened: the effect ran once, and its two reads (first, last) filed it in the
ledger because activeEffect was set while they happened. Writing either re-runs it; writing
visits doesn't, because the effect never read it - precision without declaration. No
dependency array, no subscribe calls: the reads are the truth about what the function needs, so
the reads are what's tracked.
If you've read the frontend guides, name what you just built: Vue's template tracking works this
way (reads through the proxy register the template), Svelte's $effect auto-tracking works this
way, Angular's signal graph works this way. Different syntax, same activeEffect at the center.
Your turn: the stale-subscription hunt
One subtlety separates your effect from production ones. Predict first, then run:
// (compact copy of the machinery)
const ledger = ; let activeEffect = null;
const state = ;
;
state. = false; // effect re-runs, prints "Please log in"
state. = 'Grace'; // QUESTION: does the effect re-run? Should it?
What just happened: it re-ran - and printed "Please log in" again, uselessly. The first run
subscribed to name; after loggedIn flipped, the effect doesn't read name anymore, but the
old subscription is still filed. Production frameworks fix this by clearing an effect's old
subscriptions before every re-run, so each run's reads define its dependencies fresh. That's
"dependencies are what you read last time" - a line you can now read in Vue's or Svelte's
source and nod at. (Implementing the cleanup is a great stretch exercise: give each effect a list
of the Sets it's been added to, and empty them before re-running.)
computed(): a cached effect with a value
A derived value is an effect that produces something instead of doing something - plus a cache:
const ledger = ; let activeEffect = null;
const cart = ;
const total = ;
console.log; // recomputes
console.log; // cached - no recompute line
cart. = 3; // marks dirty (via the inner effect)
console.log; // recomputes once
console.log; // cached again
What just happened: the dirty flag is the whole idea of computed in every framework - lazy
recomputation. Reading a clean cache is free; a dependency write only flips the flag; the next
read pays the recompute once. Ten template references to a computed cost one calculation - the
exact behavior our React (useMemo), Vue (computed), Svelte ($derived), and Angular
(computed) guides described from the outside. (Production versions propagate "dirtiness"
through chains of computeds more cleverly - the flag is the real core.)
Recap
- The trick: a global
activeEffectset while a function runs;gettraps file it. Reading is subscribing. - Precision falls out: effects re-run only for properties they actually read.
- The branching-effect exercise shows why real frameworks re-track on every run - dependencies are last run's reads.
computed= effect + cache + dirty flag: lazy, cached derivation.- You have now personally implemented the sentence "the framework tracks your dependencies."
← Phase 1: Reactive Objects · Guide overview · Phase 3: The Virtual DOM →