I’m trying out hyperactiv[1], a proxy based observable system that reminds me of Vue and Svelte.
- Vue and Svelte are designed to update the DOM, but I often want to work with Canvas/WebGL drawing.
- Hyperactiv uses proxies, which Vue was supposed to get in early 2019 before they greatly expanded the scope of Vue 3.
- Hyperactiv is much smaller than Vue, because it does so much less. And it can be loaded from a CDN, unlike Svelte.
I set up a simple example here that redraws the canvas with flicker to make it obvious it’s being redrawn:
Here’s the setup code:
const { computed, observe } = hyperactiv; function getContext(id) { const canvas = document.getElementById(id); canvas.width = canvas.height = 100; const ctx = canvas.getContext('2d'); return ctx; } const ctx1 = getContext('canvas1'), ctx2 = getContext('canvas2'), ctx3 = getContext('canvas3');
Now to the good stuff. We can wrap objects we want to watch with observe
:
let settings = observe({ x: 5, y: 10, w: 3, h: 5, });
Then we can set up actions using computed
:
computed(() => { const {x, w} = settings; const hue = 30 + Math.random() * 60 | 0; ctx1.fillStyle = `hsl(${hue}, 50%, 50%`; ctx1.fillRect(0, 0, 100, 100); ctx1.fillStyle = "black"; ctx1.fillRect(x - w/2, 0, w, 100); });
This action will redraw the canvas, but only when the settings
object is changed. But not any field! I only use the x
and w
fields of the object, so it will call this redraw function only when one of those two fields changes. Try it yourself: move the x
slider and watch the first canvas is redrawn but the second one is not. x=
Let’s add another redraw function:
computed(() => { const {y, h} = settings; const hue = 30 + Math.random() * 60 | 0; ctx2.fillStyle = `hsl(${hue}, 50%, 50%`; ctx2.fillRect(0, 0, 100, 100); ctx2.fillStyle = "black"; ctx2.fillRect(0, y - h/2, 100, h); });
This one only uses y
and h
. Try it yourself: move the y
slider and watch the second canvas is redrawn but the first one is not. y=
Let’s set up a third one that uses x
and y
but not w
or h
:
computed(() => { const {x, y} = settings; const hue = 30 + Math.random() * 60 | 0; ctx3.fillStyle = `hsl(${hue}, 50%, 50%`; ctx3.fillRect(0, 0, 100, 100); ctx3.fillStyle = "black"; ctx3.fillRect(x - 2, y - 2, 5, 5); });
Moving either slider will redraw this diagram.
I think this is pretty nice. You can set a variable settings.x = 30; and it will automatically redraw the canvases that use x
.
But wait! Unlike Vue and Svelte, this doesn’t do anything with the DOM. That means it didn’t tie into the sliders. Let’s add that:
for (let field of ['x', 'y', 'w', 'h']) { let inputs = document.querySelectorAll(`input[name=${field}]`); computed(() => { for (let input of inputs) { input.value = settings[field]; input.addEventListener('input', function() { settings[field] = this.valueAsNumber; }); } }); }
Now if we change the setting anywhere, it will update the slider to match. It works with multiple sliders tied to the same variable, too. Try changing x
with any of these controls:
I can do the same for output values:
for (let field of ['x', 'y', 'w', 'h']) { let outputs = document.querySelectorAll(`output[name=${field}]`); computed(() => { for (let output of outputs) { output.innerText = settings[field]; } }); }
My initial impression is that the 1kB hyperactiv library seems useful for some of my projects. With minimal code I can connect sliders to redraw just the things that need to be redrawn without having to explicitly list dependencies. And I can also automatically output values to the DOM without much effort. And it seems like it could work well with the 3kB lit-html[2] DOM library (see discussion[3]). However, it doesn’t work with Map and Set types; it only observes regular objects and arrays.
I don’t have any reason to believe this library is better than others, and I should consider looking at:
- https://github.com/salesforce/observable-membrane[4] - used by Salesforce in production apps
- https://github.com/nx-js/observer-util[5] (4kB) which handles objects, arrays, Map, and Set, and also gives control over when the observers run.
- https://github.com/vuejs/core/tree/main/packages/reactivity[6] (8kB) which handles objects, arrays, Map, and Set, and was designed for use with Vue. It can be used outside of Vue too, like in alpine.js and https://www.owlsdepartment.com/blog/using-vue-without-actually-using-vue/[7]
- https://github.com/sindresorhus/on-change[8]
- https://github.com/stackblitz/alien-signals[9] - goal is to be fast, and was ported to Vue 3.6
There are also other small libraries that provide related functionality. For example, virtual dom