DEV Community

Cover image for Implementing Reactivity from scratch
Siddharth
Siddharth

Posted on • Edited on

Implementing Reactivity from scratch

Reactivity is at the heart of many web interfaces. It makes programming robust and interactive web apps much, much easier. Although most frameworks have reactivity as a built in feature, there will always be a point when you need reactivity in plain JavaScript. So, here I will show you how to implement reactivity in JavaScript.

Wait... What is reactivity?

There are a bunch of explanations out there, the best one so far being this. But here, I'll show you a code sample, which is easier to understand.

Suppose you have this:

let who = 'Siddharth';

document.querySelector('h1').innerText = who;
Enter fullscreen mode Exit fullscreen mode

Later, you change who:

who = 'Somebody';
Enter fullscreen mode Exit fullscreen mode

But the content in the H1 does not change until we call document.querySelector('h1').innerText = who; again. This is where reactivity comes in. It automatically reruns the code (in our case document.querySelector('h1').innerText = who;) when the referred variables change. So, when we change the variable, the change is automatically reflected in the code.

The engine

Note: to keep this tutorial simple (and fun!), I won't implement error handling, objects, and all the boring checks. The next parts of this tutorial (if I write them!) will go in detail on some of them.

First, let's build an object which we need to react to:

let data = {
    name: 'John Doe',
    age: 25
};
Enter fullscreen mode Exit fullscreen mode

One way to make it reactive would be to have setters/getters to listen for events, and react to that.


A quick note on setters/getters.

Getters and setters are functions which are called when an object's property is called/set. Here's a simple example:
const obj = {
    data: [],
    get foo() {
        return this.data.join(', ');
    },
    set foo(val) {
        this.data.push(val);
    }
}

obj.foo = 1;
obj.foo = 2;
obj.foo = 3;

obj.foo; //=> 1, 2, 3
Enter fullscreen mode Exit fullscreen mode
Setters and getters are really helpful when building reactivity

So, we would need to change the object to be like this:

let data = {
    name: 'John Doe',
    get name () {
        return this.name;
    },

    set name (val) {
        this.name = name;
        // TODO notify
    }
};
Enter fullscreen mode Exit fullscreen mode

And code using it would look like this:

const data = new Reactive({
    name: 'John Doe',
    age: 25
});

data.listen('name', val => console.log('name was changed to ' + val));

data.contents.name = 'Siddharth';
//=> name was changed to Siddharth
Enter fullscreen mode Exit fullscreen mode

So, let's first build the Reactive class:

class Reactive {
    constructor(obj) {/* TODO */}
    listen(prop) {/* TODO */}
}
Enter fullscreen mode Exit fullscreen mode

the constructor is quite simple, just set the data and start observing:

constructor (obj) {
    this.contents = obj;
    this.listeners = {}; // Will be explained later
    this.makeReactive(obj);
}
Enter fullscreen mode Exit fullscreen mode

Now, we'll implement makeReactive:

makeReactive(obj) {
    Object.keys(obj).forEach(prop => this.makePropReactive(obj, prop));
}
Enter fullscreen mode Exit fullscreen mode

Now, we'll implement makePropReactive:

makePropReactive(obj, key) {
    let value = obj[key]; // Cache

    Object.defineProperty(obj, key, {
        get () {
            return value;
        },
        set (newValue) {
            value = newValue;
            this.notify(key);
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Here, we use Object.defineProperty to set getters on an the object.

Next thing to do is set up a notifier and an listener. The listener is pretty simple:

listen(prop, handler) {
    if (!this.listeners[prop]) this.listeners[prop] = [];

    this.listeners[prop].push(handler);
}
Enter fullscreen mode Exit fullscreen mode

Here, we set listeners on an object as values in an array.

Next, to notify:

notify(prop) {
    this.listeners[prop].forEach(listener => listener(this.contents[prop]));
}
Enter fullscreen mode Exit fullscreen mode

And that's the end! Here's the full code:

class Reactive {
    constructor (obj) {
        this.contents = obj;
        this.listeners = {};
        this.makeReactive(obj);
    }

    makeReactive(obj) {
        Object.keys(obj).forEach(prop => this.makePropReactive(obj, prop));
    }

    makePropReactive(obj, key) {
        let value = obj[key];

        // Gotta be careful with this here
        const that = this;

        Object.defineProperty(obj, key, {
            get () {
                    return value;
            },
            set (newValue) {
                value = newValue;
                that.notify(key)
            }
        });
    }

    listen(prop, handler) {
        if (!this.listeners[prop]) this.listeners[prop] = [];

        this.listeners[prop].push(handler);
    }

    notify(prop) {
        this.listeners[prop].forEach(listener => listener(this.contents[prop]));
    }
}
Enter fullscreen mode Exit fullscreen mode

Simple, isn't it? Here's a repl:

// Setup code class Reactive { constructor (obj) { this.contents = obj; this.listeners = {}; this.makeReactive(obj); } makeReactive(obj) { Object.keys(obj).forEach(prop => this.makePropReactive(obj, prop)); } makePropReactive(obj, key) { let value = obj[key]; // Gotta be careful with this here const that = this; Object.defineProperty(obj, key, { get () { return value; }, set (newValue) { value = newValue; that.notify(key) } }); } listen(prop, handler) { if (!this.listeners[prop]) this.listeners[prop] = []; this.listeners[prop].push(handler); } notify(prop) { this.listeners[prop].forEach(listener => listener(this.contents[prop])); } } const data = new Reactive({ foo: 'bar' }); data.listen('foo', (change) => console.log('Change: ' + change)); data.contents.foo = 'baz';

Thanks for reading! In the next parts, we'll get a bit more into how we can enhance this.

Top comments (16)

Collapse
 
grahamthedev profile image
GrahamTheDev

REPL isn't working but other than that this is well written and clear!

Most people haven't heard of getters and setters in JS.

Your next one should be on proxy as an alternative way to do the same / similar.

Great article! ❤🦄

Collapse
 
siddharthshyniben profile image
Siddharth

Yeah, the REPL has a lot of problems. I actually opened two issues on it the last day.

Proxy is not that supported, so I chose getters/setters.

Collapse
 
intermundos profile image
intermundos

What do you mean proxy not supported? Proxy is much supported. In fact, vue 3 reactivity is base on Proxy.

Thread Thread
 
siddharthshyniben profile image
Siddharth

It's not supported everywhere

Thread Thread
 
intermundos profile image
intermundos

Yes, for sure. Only if you support some esoteric platforms keep writing getters and setters.

Collapse
 
grahamthedev profile image
GrahamTheDev

Proxy is not that supported, so I chose getters/setters

But you are using arrow functions so the difference will be next to nothing!

REPL seems to work now!

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

TBF arrow functions can be provided by babel, proxy cannot be polyfilled. I don't use them either because of the need for IE11 support.

Collapse
 
merichard123 profile image
Richard • Edited

This is really well written! Thank you for the explanation!!

Just checking:

set name (val) {
        this.name = name;
        // TODO notify
 }

Should this have been this.name = val taking the value from the setter param?

Collapse
 
siddharthshyniben profile image
Siddharth

Welcome 😁

Collapse
 
toraritte profile image
Attila Gulyas

I got bogged down at the very beginning at const who =, especially when I read "Later, you change who"... I was under the impression that JS constants cannot be changed, and it also didn't work for me when I tried, but I may be missing something basic.

Collapse
 
siddharthshyniben profile image
Siddharth

That's my bad, constants can't be changed. I'll fix it. Thanks!

Collapse
 
johnwarner profile image
John Warner

Nice article. The Modstache library I wrote uses this approach for reactivity. It uses Object.defineProperty to modify object property getter and setter functions to automatically modify the DOM when the property is modified. It also uses a Proxy for array manipulations. It's an efficient way to detect changes to an object and perform an action, log activity, etc.

Collapse
 
zyabxwcd profile image
Akash

noice

Collapse
 
souksyp profile image
Souk Syp.

Cool! Look similar to Observer pattern

Collapse
 
tojacob profile image
Jacob Samuel G.

It's beautiful. I had never thought of reactivity in this way. Thanks.

Collapse
 
alisher profile image
Alisher-Usmonov

I think that codes needs some improvement for wroking with primitive values