Let's go reactive with VanillaJS.

My last blog about ditching state managers made me think about Vue's ref and SolidJS signals that are so hot right now that even Angular made their own signal implementation. But do we really need a fancy framework to jump on this craze or can we achieve something with just vanilla javascript?

Well look no further because this time we will create a framework of our own called OdéJS!

Here comes Odé!

Javascript has this thing called Proxies, but reading the documentation it doesn't really sound like it was designed for this job. With proxies you can validate new values or format values when they are requested, for example. I happen to know that Vue uses proxies for reactivity so that's enough proof for me!

In a nutshell when we create data driven apps, we want the UI to change when the data changes. Now if we set a function to react when a data property changes in a object, we have achiveved our goals! Simple!

const query = document.querySelector.bind(document)

function createProxy(initialValue, onChange) {
    // the first argument is the target object where
    // we set our initialValue
    // these properties could be named anything and there
    // could be as many as we want but this way we look like
    // Vue's refs or SolidJS signals.
    return new Proxy(
        {
            value: initialValue,
        },
        {
            // this function is always called by
            // the Proxy whenever the target object
            // value changes
            set(obj, prop, value) {
                // we only care about value here
                if (prop === 'value') {
                    // actually set the value to the target object
                    obj[prop] = value;
                    // call our callback
                    onChange(obj[prop])
                }
                // return true if you accept the incoming value
                return true;
            }
        }
    )
}

const seconds = createProxy(0, (val) => {
    // update the DOM
    query('#seconds').innerHTML = val;
})

const loop = () => {
    seconds.value += 1;

    setTimeout(() => loop(), 1000)
}

loop()

We update the DOM only when the data changes and we update it precisely where it needs to. Surely there can't be more efficient way than this?

The above is a very simple example and it only allows for a single listener for when the data changes. We want to work with multiple modules and have multiple listeners. We also want to be able to remove listeners.

So let's create a module for our createProxy function and add the query function with it so we can now call it a framework.

export const query = document.querySelector.bind(document)

/**
 * This time only a initialValue is passed to the function
 * 
 * @param {any} initialValue 
 * @returns {Proxy}
 */
export function createProxy(initialValue) {
    // this is where we gather all the onChange callbacks
    const callbacks = [];

    return new Proxy(
        {
            value: initialValue,
            // register a callback on when the value changes
            onChange(callback) {
                const id = callbacks.length + 1;

                callbacks.push({
                    id,
                    callback,
                })
                // when a callback is registered, return a function
                // that removes the callback using the id set above
                return () => {
                    const index = callbacks.findIndex(row => row.id === id);

                    callbacks.splice(index, 1);
                }
            },
        },
        {
            set(obj, prop, value) {
                if (prop === 'value') {
                    obj[prop] = value;
                    // just like before but now we have an array
                    // full of callback functions
                    for (let onChange of callbacks) {
                        onChange.callback(obj[prop])
                    }
                }

                return true;
            }
        }
    )
}

So now we have a great framework to implement the simple example with:

import { createProxy, query } from './ode.js'

const seconds = createProxy(0)

seconds.onChange((val) => {
    // update the DOM
    query('#seconds').innerHTML = val;
})

const loop = () => {
    seconds.value += 1;

    setTimeout(() => loop(), 1000)
}

loop()

Looks pretty nice and we could export the const seconds and register another callback in there!

Conclusion

The obvious downfall with this is when we start using arrays or objects, because they would not work like this. Adding or removing items from an array will not trigger the proxy set function. For arrays we'd have to create our own Array implementation that overrides the original. With objects we could either register a proxy for every property or make another proxy implementation where the object is the target itself and the callback gets the value and the property name that changes. That is why Vue and others have much more complicated implementations for things like these to work in the real life.

I encourage you to try and create your own version, but I wouldn't recommend to use this or that in production.

Here's a more complex demo:

Have something to say on this topic? Here's my Twitter: @opiispanen