Efficient computations with Vuejs reactive computation trees
Vue 3 has a very versatile reactivity system that tracks reactive dependencies at runtime and builds a dependency graph to propagate changes and minimize computations. It gives you the ability to write GUIs in a very practical manner. Although it was designed to build GUIs, the creators did a very modular library so the reactivity system does not depend on virtual DOM, thus it can be used with nodejs to create all sort of things. It was was not thought to deal with heavily asynchronous chains of events (like RXjs), but it is specially good at deriving state.
Lets suppose you have a very expensive computation that must be performed in all elements of an array to whatever purpose. Every time the array changes, this function must be ran again to update the values. But the thing is: usually the array does not change completely, only some of the elements change. Some may be added, some removed and some updated. If you were to write this code in the most simple way, you would need to run the function for all elements again every time something happens. You could also implement some kind of way of comparing the arrays and perform the computation only for those that changed, but doing it manualy usually leads to code that is hard to reason and compose. Let's try to do it with Vue instead and see how it goes.
I'm using vitest here, it's just like jest. ref, reactive and computed are Vue functions from the so called 'composition API', and are the only ones I'm going to need. I'm using images because I think colors are important (I'm sorry if you are on a phone, turning it sideways may help).
You can see that the updates are reactive, whenever something changes in our array, the resulting value is updated (when we try to read it). But the expensive calculation was done 7 times, in theory we only need to do it 4 times, because there are only 4 different values.
But how does Vue track dependencies?
Vue track dependencies when you access reactive objects, normal objects do not matter. We can use the debugging tools the library provides to see what is happening, similar hooks exist on the components as well:
When using computeds outside components, you should create a reactive scope so you can dispose them when you don't need them anymore, avoiding memory leaks.
Back to the optimization
One thing we can take advantage of with vue is that it is very smart and does not track nested computeds. I will show you what I mean:
Recommended by LinkedIn
We could now spare one computation. Every time we change the inner value of an element of the array, all the other elements can have they calculations saved.
What flatComputed does is to transform an array of ComputedRefs into a ComputedRef of an array. This is exactly what Promise.all does to promises.
I'm not a specialist here, but ComputedRef looks like a functor, its just that in order to map it, you need to use another computed. Several of the operations match those that libraries like Rxjs offer, it's just that the way you perform them is a bit different.
Here is an example with rxjs:
And this way we have the same 6 times.
But we still could not reach the optimal 4 times. We can do it with memoization. There are some complicated things about memoization, though: it is tricky to implement with complex objects, and it may be dangerous to use if you do not have strong guarantees that your objects are not going to change. But if you like immutable data, it is not hard. And with vue, we can put our memoized function behind a reference itself, so if the function changes, all the values are going to be calculated again. This can be a very powerful pattern.
Now we could reach the optimum value of only 4 calculations and our data is still reactive. You can notice that I don't need nested computeds to achieve this. So why should I care about them? Because arr.map(compTransformation.value) is going to run all the time. In this case it does not matter, but I could be running something expensive there too, and in this case I would need the nested computeds.
Vue is a great library. This is a very simple example but you can imagine this escalating to lots of inputs. The more data you have to derive your state, the more difficult it is to manually arrange them in an efficient and consistent way. I hope this article can give you some insights.