Reactive Objects
Every reactivity system - Vue's, Svelte's, the one you're about to write - rests on one
capability: knowing when data is read and when it's written. Plain JavaScript objects don't
report either. But the language ships a tool that wraps any object and intercepts everything done
to it: the Proxy. Today you build with it.
Meet the Proxy
A Proxy wraps a target object and routes operations through traps - functions you supply.
Two traps carry this whole project: get (a property was read) and set (a property was
written). Run it:
const user = ;
const spied = ;
// Use it like a normal object - the traps fire invisibly:
const n = spied.;
spied. = 'enterprise';
console.log;
What just happened: spied behaves exactly like user - same properties, same values - but
every access ran through your traps first. The object can now announce its own reads and
writes. That announcement is the entire foundation: a framework that hears "someone read name"
and later "someone wrote name" knows exactly which screen updates matter.
From spy to subscription ledger
Logging is a demo; a framework needs bookkeeping. The plan: when a property is read, remember who was asking (we'll wire that up properly in phase 2 - for now, a placeholder). When it's written, look up everyone who asked and notify them. The ledger:
// A two-level map: object -> (key -> Set of subscribers)
const ledger = ;
// Try the ledger on its own:
const state = ;
;
;
;
What just happened: subscribe files a function under (object, property); notify runs
everything filed there. A WeakMap keyed by the object means the bookkeeping disappears when the
object does - no leak. A Set per property means the same subscriber can't be filed twice.
reactive(): the two pieces joined
Now the move that makes it automatic - the proxy's traps call the ledger:
const ledger = ;
// Demo: manual subscription, automatic notification.
// Note we subscribe on the RAW object: the traps file their bookkeeping under it
// (the `target` they receive), so lookups must use the same key. Phase 2's
// automatic tracking happens inside the traps, which makes this seam invisible.
const rawCart = ;
const cart = ;
;
;
cart. = 1; // only the badge subscriber fires
cart. = 1900; // only the total subscriber fires
cart. = 1; // same value: nothing fires (check the guard in set)
cart. = 2;
What just happened: writes now notify precisely the subscribers of that property - update
items and the total subscriber stays quiet. The target[key] === value guard skips no-op
writes, which real frameworks also do (you met it as "signals compare by reference" if you've
read the Angular guide). Two loose ends remain, both deliberate: the subscribing is still manual,
and you had to know about the raw-vs-proxy seam to file subscriptions under the right key. Phase 2
fixes both at once - automatic tracking lives inside the traps, where target is always the raw
object - and it's the best trick in frontend engineering.
Your turn
Extend reactive's set trap so it also logs a warning - without notifying - when code tries to
write a property that didn't exist on the original object (a typo catcher: cart.tota = 5
should warn, not silently create a property). Object.hasOwn(target, key) tells you if the key
existed. Then prove it works:
const cart = ;
cart. = 3; // should write silently
cart. = 99; // should WARN and not create the property
console.log; // 3, false
Recap
- A
Proxywraps an object and intercepts reads (get) and writes (set) - data that can announce itself. - The ledger is a
WeakMap(object → Map(key → Set(subscriber)))- per-property precision, leak-free by construction. reactive()= proxy + ledger: writes notify exactly the right subscribers, no-op writes are filtered.- Reads are the missing half - automating "who was asking" is phase 2.
← Guide overview · Phase 2: Effects and Computed →