Implementing Reactivity from scratch
Tags: javascript, tutorial, beginners, webdev
Published: 2021-08-12 16:09:52

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](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754). But here, I'll show you a code sample, which is easier to understand.

Suppose you have this:

```js
let who = 'Siddharth';

document.querySelector('h1').innerText = who;
```

Later, you change `who`:

```js
who = 'Somebody';
```

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:

```js
let data = {
	name: 'John Doe',
	age: 25
};
```

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._ <br><br> [Getters](https://mdn.io/getter) and [setters](https://mdn.io/setter) are functions which are called when an object's property is called/set. Here's a simple example: |
|---|
```js
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
```
| Setters and getters are really helpful when building reactivity |
| --- |

---

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

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

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

And code using it would look like this:

```js
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
```

So, let's first build the `Reactive` class:

```js
class Reactive {
	constructor(obj) {/* TODO */}
	listen(prop) {/* TODO */}
}
```

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

```js
constructor (obj) {
	this.contents = obj;
	this.listeners = {}; // Will be explained later
	this.makeReactive(obj);
}
```

Now, we'll implement `makeReactive`: 

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

Now, we'll implement `makePropReactive`: 

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

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

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:

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

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

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

Next, to notify: 

```js
notify(prop) {
	this.listeners[prop].forEach(listener => listener(this.contents[prop]));
}
```

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

```js
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]));
    }
}
```

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

{% runkit %}
// 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';
{% endrunkit %}

---

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