In this post, first published in patak.dev, @patak_js explores how to use
markRaw to opt-out of reactivity, reviewing an optimization applied to the VueUse’s
useManualRefHistory composable. This is the third chapter in the Ref History Adventures Series. If you didn’t read them yet, we recommend to check out the previous articles first. We hope you enjoy it, let us know what you think at @Leniolabs_.
Ref History Adventures
Vue 3 reactivity just works. The change detection caveats that we had to learn in Vue 2 are now gone. If you have a reactive object or array, you can assign or push new properties and everything will fall in place. New proxies will be created making the inserted object reactive. You may feel that there is no need to think that much anymore about what is going on under the hood. But there are cases where making every inserted object reactive is not the right choice. If the new objects are immutable for example, a change will never be triggered but we are paying a price for handling reactivity anyway.
The reactivity system provides tools to opt out of reactivity in these cases. With
markRaw we can tell Vue that a particular tree of objects doesn’t need to be tracked, avoiding the performance hit. There are also other handy utilities like
readonly, that creates readonly proxies to objects or Refs. In this post, we will look at a real example where
markRaw was used to optimize a composable in VueUse.
For context, you can read the previous two posts in this series. In Ignorable Watch and History and Persistence we dived into how VueUse’s
useRefHistory is implemented and how it can be combined with other composables.
A new reusable piece, useManualRefHistory has also been spawned.
useManualRefHistory offers the same API as the auto-tracking
useRefHistory, but only generates snapshots when
commit() is called. It lets users add undo support to their apps that integrates with their operation abstractions.
useManualRefHistory can be used together with
useLocalStorage in the same way that is described in [History and Persistence](./history-and-persistence].
useRefHistory is now coded in terms of
useManualRefHistory, together with VueUse’s
pausableFilter utilities. The logic only deals with auto-tracking at this point.
This is a new composable, instead of a
manual option in
useRefHistory so users do not have to pay for features they do not use. The manual version is half the size of the auto-tracked composable.
Let’s look at a simplified version of
useManualRefHistory to discuss an important optimization when dealing with reactive state.
useManualUndo will only keep track of past history and provide an
undo function to go back to previous states.
clone is a utility function that could be implemented piping
When we add the snapshot to the
history ref array, what is pushed is a reactive version of it. But once we take a snapshot, it will no longer be mutated. If the
source holds big objects, we will be paying for a lot of unneeded reactive objects. We could decide to avoid using reactivity altogether for the
history array but being able to watch for changes to it is an important feature
This is a good use case for
markRaw. We can use it to indicate to Vue that the object returned by
snapshot() doesn’t need to be reactive. When this marked object is added to the
history array, it will no be transformed into a reactive object.
If we watch for changes in the
history array, the effect will be triggered normally when a snapshot is added to it. But the reactivity system will not longer care if there is a change to the snapshot object itself.
When we need to expose these raw objects to other composables independently of other reactive objects, instead of
markRaw we have
shallowRef available that creates a ref that tracks its own
.value mutation but internal changes behave as if the object was marked with