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

Part I → Ignorable Watch
Part II → History and Persistence
Part III → Mark Raw Optimization

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.

    const state = ref({ posts: [], authors: [] })
    
    // Immutable data
    const post = { ... }
    
    // Insert the post in a reactive array
    state.value.posts.push( post )
    
    // state.value.posts[0] is reactive

The reactivity system provides tools to opt out of reactivity in these cases. With shallowRef, and 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.

Manual History

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.

    import { ref } from 'vue' 
    import { useManualRefHistory } from '@vueuse/core'
    
    const state = ref({ foo: 1, bar: [] })
    const { history, commit, undo } = useManualRefHistory(counter, { clone: true })
    
    // Integrate with your operation abstractions
    operations.subscribe(commit)
    
    // Or directly create snapshots manually
    state.value.foo += 1
    state.value.bar.push({ id: 3 })
    commit()

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 ignorableWath and 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.

Raw History

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 JSON.parse and JSON.stringify.

    import { clone } from 'utils'
    
    function useManualUndo(source) {
    
      function snapshot() {
        return clone(source.value)
      }
    
      const history = ref([ snapshot() ])
      
      function commit() {
        history.value.unshift( snapshot() )
    
        // history.value[n] is reactive
      }
    
      function undo() {
        history.value.shift()
        source.value = clone(history.value[0])
      }
    
      return { history, commit, undo }
    }  

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

    <template>
      <button :disable="history.length > 1" @click="undo()">
        Undo
      </button>
    </template>

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.

    import { clone } from 'utils'
    import { markRaw } from 'vue'
    
    function useManualUndo(source) {
    
      function snapshot() {
        return markRaw( clone(source.value) )
      }
    
      const history = ref([ snapshot() ])
    
      function commit() {
        history.value.unshift( snapshot() )
        // history.value[n] is *not* reactive
      }
    
      ...

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 markRaw.