loading...

Reactive primitives in JavaScript (and other cool stuff with OxJS)

jfet97 profile image Andrea Simone Costa ・6 min read

Ok ok, I know what you are thinking: this is impossible!

 

And the delirium began

Some time ago I was wondering if it were possible to create direct reactive relationships between entities in JavaScript.
In other words I would have liked to be able to write code conceptually represented by the following pseudo one:

// create a source
const $source = { foo: 42 };

// create a reactive primitive that will be updated
// when the 'foo' field of the source changes
rx primitive = $source.foo;
primitive; // 42

// create a reactive object that will be updated
// when changes happen into the source
rx object = $source;
object; // { foo: 42 };

// change the 'foo' field source
$source.foo = 'hi';

// the reactive primitive was updated
primitive; // 'hi'
// the reactive object as well
object; // { foo: 'hi' }

// change the whole source
$source = { foo: { bar: 'baz' } };

// the reactive object was updated
object; // { foo: { bar: 'baz' } }
// the reactive primitive too, but unfortunately is no more a primitive
primitive; // { bar: 'baz' }

What's the point of such type of reactivity, that I like to define encapsulated reactivity or low-level reactivity?
It helps to observe changes inside a structure that could easily become the source of truth in event-driven applications.

Let's talk about VueJS computed properties, from which I've took inspiration to build the raw reactivity system of OxJS. I'm not going to explain VueJS reactivity system here, but I can link this interesting video series that contains lot of useful information.
For each Vue component, we can consider the union of the internal data object and the props that the component's parent has passed to it as the source of truth:

export default {
    data() {
        // the internal data object
        return {
            age: 22
        }
    },

    props: {
        // a numerical multiplier prop
        multiplier: {
            type: Number,
            default: 1,
        }
    },

    // here the magic
    computed: {
        result() {
            return this.multiplier * this.age + 1;
        }
    }
}

Computed properties are a special type of properties that result from combining other properties of the component with some custom logic. In our example the result computed property will become the result of a mathematical operations that uses the multiplier prop and the age data property.
Every time one of those changes, the result property will be recomputed, hence the name, updating properly.

For VueJS developers computed properties are very useful and irreplaceable; the lifeblood of each VueJS component, because they make reactivity a breeze.

So I've asked myself: why not try to extract, broadly, this type of reactivity from VueJS? A few days later OxJS was born.

Epic music

OxJS is a proof of concept library written in TS that enable encapsulated reactivity.

Let's try it to create an observable and a reactive object:

const { ox } = require('oxjs');

// it creates an observable source
const $source = ox.observable({
    years: 32,
});

// it creates a reactive object
const reactiveObject = ox.observer(() => {
    // we are referencing the observable here
    const years = $source.years;

    // an object is returned
    // so we are creating a reactive object
    return {
        age: years,
    }
});

// initial evaluation
console.log(reactiveObject); // { age: 32 }

// we change the stored 'years' inside $source
$source.years = 28;

// the 'reactiveObject' is updated
console.log(reactiveObject); // { age: 28 }

As you can see the creation of both an observable and an observer is pretty straightforward; moreover the latter one is notified as soon as possible.
Obviously we are not limited to one observer for one observable at time: an observable can be used by any number of observers and an observer can use how many observable it needs.

What about reactive primitives?

Here it is:

const { ox } = require('oxjs');

// it creates two observable sources
const $source1 = ox.observable({
    years: 32,
});

const $source2 = ox.observable({
    name: 'Mario',
});

// it creates an observer that will behave as a string
const stringObserver = ox.observer(() => `My name is ${$source2.name} and I'm ${$source1.years} years old`);

// initial evaluation
console.log(`${stringObserver}`); // My name is Mario and I'm 32 years old

// we change the stored 'years' inside $source1
$source1.years = 28;

// the 'stringObserver' is updated
console.log(`${stringObserver}`); // My name is Mario and I'm 28 years old

// we change the stored 'name' inside $source2
$source2.name = 'Luigi';

// the 'stringObserver' is updated
console.log(`${stringObserver}`); // My name is Luigi and I'm 28 years old

As you will see my reactive primitives are not perfect, because they are not real primitives. Otherwise my API would not have been able to change the value referenced by them. In fact they are based on primitives wrappers and a weird ES6 Proxies hack that has got some limitations, due to the JS lang itself.

But they are suitable for a wide range of cases if you pay a little attention. Most times you will not feel the difference. I hope.

What's going on under the hood?

I'm not able to explain all the reactive hackish that I've proudly written, but I can try to describe the magic that happens when a reactive entity needs to be updated.

Let's suppose this is our source of truth:

const $source = ox.observable({
    city: 'Carrara',
    country: 'Italy',
});

We call into question the following weird observer that could be either a string or an object:

let weirdObserver = ox.observer(() => {
    const city = $source.city;
    const country = $source.country;

    if (city && city.length > 5) {
        return `${city} is located in ${country}`;
    } else {
        return {
            city,
            country
        }
    }
});

// initial evaluation
console.log(`${weirdObserver}`); // "Carrara is located in Italy"

Knowing that each time one of the used $source fields changes, the arrow function passed to ox.observe is called, the main problem was: how to change on what the weirdObserver identifier is pointing to?
Without relying on a closure, which would have led to a change in the way ox.observe and OxJS were thought to be used, there is no way in JS.

Therefore, if we cannot move the weirdObserver identifier, we can't even set it to a primitive, because in JS two different identifiers cannot point to the same memory area if in it a primitive value is stored.

Wait a moment, why we need another reference?

Because if weirdObserver is immovable, we need another reference to the same thing pointed by it - reference that will be stored somehow by OxJS - to perform the changes, so that weirdObserver "sees them" as well, so to speak.

To summarize what has been said so far:

  • no primitives are allowed (here's why I do use primitive wrappers)
  • weirdObserver is immovable and will always point to the same object
  • there is another reference to the same object pointed by weirdObserver that is used to perform changes

Now another another problem comes up: that object should be able to completely change its nature. What if it should be transformed from an Array to a Number wrapper, to be then changed into a Promise, passing from a Map?
Yes I'm exaggerating, but I believe you have now grasped the point: there is no merge strategy nor prototypes hack which could help us.

So what? ES6 Proxies!

The solution I found is to return an almost fully transparent Proxy on which the target is dynamically changed at runtime. As a rule this is not possible, unless all traps are redirected.
I know, this is a big, ugly workaround. But I was able to made the hack resilient for the most use cases.

Returning to our example, the proxy referenced by weirdObserver will have a String wrapper as a target initially. Each time the source changes, even if the length limit is not exceeded, the target of the proxy will change.

If length of the new $source.city value is greater than 5, the new target will be a new String wrapper, otherwise will be a new { city, country } object. Due to the nature of Proxies, the client, that is who use the weirdObserver identifier, will be able to use all the String.prototype's methods in the former case and to perform almost all the operations which are allowed on an object in the latter.

Other things that OxJS can do

I'm too lazy to create a clone of the README. Please check it out here.

Conclusion

As I said this library is only a proof of concept, IDK if this sort of reactivity could be suitable in real applications without having a framework that protect us. Maybe its "implicit" nature could quickly generate problems if misused.
I'd like to hear your opinion on that.

Moreover sometimes I like to develop only for the pleasure of it, without worrying too much on clean and efficient code nor wondering on real use cases of what I'm creating. This is one of those cases; I've focused more on trying to upgrade my TS skills (apparently with little success seeing how much I had to fight 😂) and on reaching my main goal: take reactivity to a greater level of mess.

Discussion

pic
Editor guide