Implementing Reactivity from scratch

Siddharth - Aug 12 '21 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .