[フレーム]
Tech · 26 September 2025 · 4 min read

Using proxy object for state management

A recent internal talk by Jessica Holding about Angular Signals made me wonder if I could get a similar experience using native JavaScript functionality. I have long advocated about careful consideration of any third-party library added to your code, and with the recent supply chain attack on a third-party library with 2 billion weekly downloads, I think it’s a good opportunity to play around and see if we can replicate some of Angular’s "black magic". In my opinion, JavaScript’s Proxy object doesn’t get the recognition it deserves. Simply put, it allows to add seamless code to an Object’s getters and setters. This means we can perform additional operations whenever we read or write a value. Consider the following code:

 constdata = {};
 conststate = newProxy(data, { get(target, prop) { returntarget[prop] * 2; } });
 state.value = 8;
 console.log(data.value, state.value); // 8 16

We’re creating an empty ​data object and wrapped with a Proxy that has a getter that doubles the returned value. The data.value​ doesn’t change but whenever we’ll try to access it via the proxy we’ll get the doubled value. Alternatively, we can manipulate the values as they’re being written - ​

 constdata = {};
 conststate = newProxy(data, {
 set(target, prop, value) { 
 return(target[prop] = value * 2); 
 }
 });
 state.value = 8;
 console.log(data.value, state.value); // 16 16

So let’s build a state machine: We’ll start off with a computed​ value, that is a readonly​ value that is a result of a function

functionState(initial = {}) {
 constcomputed = newMap();
 consthandler = {
 get(target, prop, receiver) {
 if(computed.has(prop)) {
 returncomputed.get(prop)(receiver); // compute dynamically
 }
 returnReflect.get(...arguments);
 }
 };
 constproxy = newProxy(initial, handler);
 proxy.compute = (prop, fn) =>computed.set(prop, fn);
 returnproxy;
}

And we can use it like this -

conststate = newState({ count: 0});
state.compute('doubleCount', s =>s.count * 2);
State.count++;
Console.log(state.doubleCount); // 2

Now our state has a state.doubleCount​ value that is being calculated on the fly. Note that we can use the ++​ operator and it would work just fine, and note that doubleCount​ isn’t a function but an actual object’s property;

We can also add listeners (an effect in the world of angular) so whenever a value changes, something else will be triggered.

functionState(initial = {}) {
 constlisteners = newMap();
 constnotify = (key, value) =>{
 if(listeners.has(key)) {
 listeners.get(key).forEach(fn =>fn(value));
 }
 };
 consthandler = {
 set(target, prop, value, receiver) {
 constresult = Reflect.set(...arguments);
 notify(prop, value);
 returnresult;
 }
 };
 constproxy = newProxy(initial, handler);
 proxy.addListener = (prop, fn) =>{
 if(!listeners.has(prop)) {
 listeners.set(prop, newSet());
 }
 listeners.get(prop).add(fn);
 };
 proxy.removeListener = (prop, fn) =>{
 if(listeners.has(prop)) {
 listeners.get(prop).delete(fn);
 }
 };
 returnproxy;
}

And we can use it like this to update the display whenever a value changes -

state.addListener('name', newValue =>{
 const text = newValue.length > 0 ? `Welcome, ${newValue}!`: 'Enter your name';
 document.getElementById('welcomeMessage').textContent = text;
});

Finally we’d like to create a bi-directional binding between an input field and variable. We’ll add a listener to the field that will update the variable and add a proxy listener to update the field whenever the variable changes.

proxy.bidi = (prop, elm, attribute = 'value', event = 'input') =>{
 elm[attribute] = proxy[prop] || '';
 elm.addEventListener(event, () =>proxy[prop] = elm[attribute]);
 proxy.addListener(prop, (newValue) =>{
 if(elm[attribute] !== newValue) {
 elm[attribute] = newValue || '';
 }
});
};

And then we can use it to bind the name​ element with the name​ variable - state.bidi('name', document.getElementById('name'));

Of course, Angular adds much more functionality. It provides an advanced mechanism to refresh only the relevant part of the page in case of state changes. It’s easy to refresh the entire page but it’ll have performance costs. Instead, I suggest updating the specific elements that need changing.

You can see a demo of this proxy-based State here (or its source code)

[フレーム]

Deciding which framework is the right one for your project, or which framework you should use is not a lightweight call to make. Uncle Bob also cautions about the commitment such decision requires . But should you decide to use native JS, it still doesn’t mean you need to reinvent the wheel, and I hope this article will inspire you to find a simple solution that works for you.

Read more

In this talk I'll review how the functional programming style of frameworks such as React, ImmutableJS and Redux have paved the way for novel techniques that once again support rapid development cycles
Colin Eberhardt · 7th Jul 2016
Recently, Knockout's original author Steve Sanderson released a plugin called knockout-projections which optimises the performance of the observable array methods filter and map.
Chris Price · 28th Feb 2014 · 5 min read

Want to receive more insights?

If you enjoyed this blog post, why not subscribe to our mailing list to receive Scott Logic content, news and insights straight to your inbox? Sign up here.
Oded Sharon

I am a developer at Scott Logic, Edinburgh. Find me on Github

AltStyle によって変換されたページ (->オリジナル) /