Observability and reactivity have mostly been reserved for JavaScript frameworks in the past, but thanks to modern JavaScript, we can observe the state of an object and react to changes made to it.
In this tutorial, we’re going to use one of my favourite modern JavaScript features: Proxies, to create a global, observable object that can be used as a lightweight, basic state management tool.
The end result is a meagre 132 bytes minified (128 bytes gzipped), which is the sort of lightweight JavaScript that I love.
Here it is in its entirety:
- Code language
- javascript
window.subscribers = []; const state = new Proxy(typeof defaultState !== 'undefined' ? defaultState : {}, { set(state, key, value) { const oldState = {...state}; state[key] = value; window.subscribers.forEach(callback => callback(state, oldState)); return state; } });
Let’s dig into how it all works, but first of all, we need to understand Proxies.
What are Proxies and how do they work?permalink
A Proxy object allows us to add custom behaviours to an object. We do this by defining a handler, which is where we set traps. This all sounds a bit dramatic, so I’ll create an analogy to hopefully make it a bit more understandable.
Consider a nightclub that can only have 100 people in it at any time. It has a queue of people outside which is 30 people long and inside the club there are 90 people. At the door, there is a security guard who has a count of the people currently in the club and for each person that tries to enter, they check that this number hasn’t reached 100 yet.
Because there are 90 people in the club, the security guard runs this process 10 times and waves the people through. This now means that there are 100 people in the club so no more can enter. Because of this, the next person that tries to enter gets stopped.
If someone leaves the club, the number goes down, so that means more can enter. The security guard observes the number and runs another process which calls people forward to enter as other people leave. All of this creates a nice, fluid system that keeps the maximum amount of people in the club where possible.
Let’s now translate this into JavaScript.
We define our object as usually would without Proxies, like this:
- Code language
- javascript
const defaultState = { guests: 90 };
Now lets create our security guard: AKA the Proxy:
- Code language
- javascript
const state = new Proxy(typeof defaultState !== 'undefined' ? defaultState : {}, { set(state, key, value) { if (key === 'guests' && state.guests >= 100) { throw new Error('No more guests allowed'); return state; } state.guests = value; return state; } });
This is where the security guard is doing their core job: making sure too many people can’t enter the club.
In the context of this Proxy, we are using what’s called a trap—specifically a set trap. This particular trap monitors each time outside code tries to modify a property of the object, so the JavaScript that triggers this trap should look very familiar:
- Code language
- javascript
state.guests = 93;
We need to observe the guest count and we can do that within our Proxy set trap. We need to do some groundwork, first.
The first thing we’ll do is set an array of subscriber functions on the window
like this:
- Code language
- javascript
window.subscribers = [];
Then inside our set trap, we can publish the state changes.
- Code language
- javascript
set(state, key, value) { if(key === 'guests' && state.guests >= 100) { throw new Error('No more guests allowed'); return state; } state.guests = value; // Publish the state changes to the subscribers window.subscribers.forEach(callback => callback(state)); return state; }
This means we can do something like this now:
- Code language
- javascript
const guestWatcher = state => { if (state.guests <= 100) { alert('More guests can now enter'); } };
It’s at this point that our security guard would wave some more people in.
Applying this process to create a global state systempermalink
Now that you hopefully understand how Proxies work at a surface level, you can probably see how we apply an almost identical approach to create our global state system.
To save you scrolling back up, here it is again:
- Code language
- javascript
window.subscribers = []; const state = new Proxy(typeof defaultState !== 'undefined' ? defaultState : {}, { set(state, key, value) { const oldState = {...state}; state[key] = value; window.subscribers.forEach(callback => callback(state, oldState)); return state; } });
Let’s break it down:
We start by creating an empty array to hold our subscriber functions. They could do anything and they will be called every time any object value changes, which is super handy.
After we define our subscribers
array on the window object, we create the Proxy. The first parameter that’s passed to a Proxy is an object. In our sample, we pass this: typeof defaultState !== 'undefined' ? defaultState : {}
.
What that does is use defaultState
if it is defined or use an empty object by default. This is really handy for setting your sensible defaults before you create the Proxy.
The second parameter is a handler. We’re only setting one trap in this handler, which is the set trap, which runs whenever a value of the proxied-object is changed.
Like we saw in the nightclub example, let’s say you have this as your defaultState
:
- Code language
- javascript
const defaultState = { name: 'Rafaela' };
When we do this: state.name = 'Tlaytmas'
, our set trap will spring into action.
Inside the set trap we take a copy of our old state:
- Code language
- javascript
const oldState = {...state};
The reason we use a spread operator is because if we did const oldState = state
, it would create a reference. This means that when state
changes: so would oldState
. Using a spread operator prevents that from happening.
- Code language
- javascript
state[key] = value;
After we take a copy of our old state, we need to apply the changes to our object. Remember: this is a trap that we’ve created, so if we don’t apply the changes that were requested, they won’t change. This is why developers often use Proxies to perform validation.
- Code language
- javascript
window.subscribers.forEach(callback => callback(state, oldState)); return state;
Lastly, we loop through the window’s subscriber functions and call them before eventually returning the new state back.
And…breathe, because we’re done. Pretty darn cool, right?
This setup is all cool in theory, but how does it look in practice? Let’s take a look.
Examplespermalink
The first example is a countdown clock. Here’s a demo, which you will probably need to click “reset” to see the effect:
See the Pen Proxy-driven global state object - Countdown by piccalilli (@piccalilli) on CodePen.
What we do here is set our default state to be 20 seconds. A renderCountdown
subscriber function is then passed into the window’s subscribers
array. This means that for each second that the runTimer()
timer takes from state, the front-end will be re-rendered.
Once the timer has reached 0, we change the HTML to a role="alert"
element to notify the user of assistive technology that the timer has completed. This also reveals a reset button which when clicked, starts the timer again.
Our second example is a good ol’ character counter that sits under a <textarea>
.
See the Pen Proxy-driven global state object - Character Counter by piccalilli (@piccalilli) on CodePen.
We trigger state changes with every input
event on the <textarea>
which renders the resulting message underneath. If the character count has not been exceeded, it shows how many you have left, in a positive state. If you exceed the character count, though, it shows how many you have gone over in an error state.
The data-element="result"
element that houses the character count has aria-live="polite"
which stops assistive technology being hounded by the constant updates. We also use aria-atomic="true"
again to determine that the region is only partially updated.
Just for fun, there’s a summary outside the form which also updates with how many characters you’ve typed. Because state is stored centrally, this is pretty darn trivial.
Wrapping uppermalink
This is just one example of how you can use Proxies to create lots of power, with tiny amounts of code. They really are a fantastic addition to JavaScript.
The central system that powers this particular example is tiny snippet of code, which in a browser context where connection speed, processor speed and contextual environment attributes are completely unpredictable, is exactly the sort of thing we need to build hi-performance sites. With that in mind: this sort of global state system probably won’t be helpful if you have a massive app, but that only accounts for a tiny minority of cases, so for most stuff, you can achieve a lot with a meagre 132 bytes of JavaScript.
I hope that this tutorial has piqued your interest in state management and state machines too, because there is a lot of good stuff to learn. Here’s a few of my recommended resources:
- Crafting Stateful Styles with State Machines by David Khourshid
- What is a state machine?
- Build a state management system with vanilla JavaScript (hey it’s me again)
- Introducing Javascript ES6 Proxies (the article that got me excited about Proxies)