kinda neat
The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object. - MDN
Here is a method for basic reactivity using a Proxy.
// our initial stateconst initialState = {}// a map of properties to their subscribersconst listeners = new Map();// our proxyconst state = new Proxy(initialState, { set(target, property, value) { const old = target[property]; target[property] = value; if(old !== value){ if (listeners.has(property)) { // if we have subscribers for this property, run them listeners.get(property).forEach(fn => fn(value)); } } return true; }});
// now our listener creatorconst listen = (property, fn) => { listeners.set(property, [...(listeners.get(property) || []), fn]);}
// we can now listen to changes on xlisten("x", (v) => { console.log("x changed to", v);});
state.x = 10; // Triggers the listenerstate.x = 10; // No trigger (same value)state.x = 20; // Triggers the listener again
But then we could get super original and wrap this in a helper function. And since we’re hip let’s call it a hook
instead of a wrapper function… let’s call it the useState
hook
let stateId = 0;const useState = (initialValue) => { const id = stateId++; const property = `state_${id}`; // Initialize the state if it doesn't exist // Remember: this is our Proxy if (!(property in state)) { state[property] = initialValue; } // Create a reactive variable return { _property: property, get value() { return state[property]; }, set value(newValue) { state[property] = newValue; } };};
Now we can use our hook to create a counter.
// Example usageconst count = useState(0);
// key is now on ._property. not the greatest API but it works.listen(count._property, (value) => { console.log("Count changed to:", value);});
// value is now on .value. not the greatest API but it works.console.log(count.value); // 0
count.value = 10; // Triggers the listenercount.value = 10; // No trigger (same value)count.value = 20; // Triggers the listener againconsole.log(count.value); // 20
We’re building our own thing so let’s make it a little more specific to our needs.
const listen = (property, fn) => { listeners.set(property, [...(listeners.get(property) || []), fn]); if (property?._property) { listeners.set(property._property, [...(listeners.get(property._property) || []), fn]); }}// an now we can just.listen(count, (value) => { console.log("Count changed to:", value);});
Then we could just turn this all into an easy to use class
class Stater { constructor(initialState = {}) { this.stateId = 0; this.listeners = new Map(); // our proxy this.state = new Proxy(initialState, { set: (target, property, value) => { const old = target[property]; target[property] = value; if (old !== value) { if (this.listeners.has(property)) { // if we have subscribers for this property, run them this.listeners.get(property).forEach((fn) => fn(value)); } } return true; }, }); } listen(stateObj, fn) { if (stateObj && stateObj._property) { this.listeners.set(stateObj._property, [ ...(this.listeners.get(stateObj._property) || []), fn, ]); } } useState(initialValue) { const property = `state_${this.stateId++}`; const listenRef = this.listen.bind(this); const stateRef = this.state; if (!(property in stateRef)) { stateRef[property] = initialValue; } return { _property: property, get value() { return stateRef[property]; }, set value(newValue) { stateRef[property] = newValue; }, // simpler access to listener creation listen(fn) { listenRef(this, fn); }, }; }}
const s = new Stater();const count = s.useState(0);
count.listen((value) => { console.log("Count changed to:", value);});
count.value = 10; // Count changed to: 10count.value = 15;// Count changed to: 15