The Magic Behind Reactivity - Proxies
This post was published 3 years ago. Some of the information might be outdated!
In this short series of blog posts, I'm going to be showing you how reactive JavaScript frameworks such as Alpine.js and Vue.js work under the hood.
We'll begin by understanding the Proxy
object in JavaScript and create our own budget version of Alpine.reactive()
.
What is Proxy
?
The Proxy
object was introduced as part of the ES2015 specification. It allows you to intercept the basic operations on an object, for example retrieving a property, setting a property or deleting a property.
You're able to define your own callbacks / handlers for these operations. This is similar to how PHP's __get
and __set
magic methods work.
You receive the target object, the name of the property being accessed and when setting, the value that's being assigned.
Here's an example:
const proxy = new Proxy(
{
count: 0,
},
{
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = value;
return true;
},
}
);
The code above shows a very basic Proxy
handler. The behaviour implemented inside of the get
and set
methods isn't anything special, it's just returning the property
from target
and setting it to value
. It's really just a regular object.
If we wanted to get fancy, we could put a console.log
inside of the get()
handler.
const proxy = new Proxy(
{
count: 0,
},
{
get(target, property) {
console.log(`Trying to access ${property}.`);
return target[property];
},
}
);
proxy.count;
When the proxy.count
expression is evaluated, the console.log
will be called and appear in your console.
One thing that confuses people a lot about Proxy
is that it doesn't change how you interact with the underlying value.
If you wrap an Array
in a Proxy
, you can still do myWrappedArray.push
or myWrappedArray.filter
.
Creating a function
Now that we know what a Proxy
is, let's create a new function:
function reactive(object) {
return new Proxy(object, {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = value;
return true;
},
});
}
One thing that we haven't covered in this function is nested Proxy
instances. Imagine the following scenario:
const user = reactive({
name: 'Ryan',
address: {
postcode: 'TT1 1TT'
}
})
// How do we intercept this!?
user.address.postcode = 'TT2 2TT';
When we update user.address.postcode
, the set
handler in our Proxy
won't be called as the address
object inside of user
isn't reactive.
Don't worry, this is easy to fix. We can recursively call reactive
for each of the properties on our original object
when they're typeof
is object
.
function reactive(object) {
if (object === null || typeof object !== 'object') {
return object;
}
for (const property in object) {
object[property] = reactive(object[property])
}
return new Proxy(object, {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = reactive(value);
return true;
},
});
}
Voila! When the object
passed into reactive
has any nested objects, they will also be passed through reactive
recursively.
When we set
the value of a property, we'll also pass the value
through reactive
to ensure that any objects are wrapped in a Proxy
too.
Wrapping up
And with that, we've created a semi-functional version of Alpine.reactive()
. The only thing that our reactive
function doesn't do yet is update or trigger any function calls.
We'll look at creating a basic version of Alpine.effect()
in the next instalment.
Until then, thank you for reading and I'll catch you next time!