Advanced reactivity APIs

Vue · Advanced · 13 min

Large data sets, third-party class instances and library code need finer control than ref and reactive give by default.

Default reactivity is deep: every nested object is wrapped in a Proxy on access. That is convenient but has costs for large or foreign data. The advanced APIs give you control.

  • `shallowRef(value)` tracks only .value assignment, not nested changes. Ideal for large immutable data (replace it wholesale: rows.value = newRows) and for holding component definitions or class instances. After mutating inside one, call `triggerRef(r)` to force an update.
  • `shallowReactive(obj)` makes only top-level properties reactive.
  • `markRaw(obj)` flags an object so Vue never proxies it: map instances, chart objects, SDK clients.
  • `toRaw(proxy)` returns the original object, useful for passing to libraries, structuredClone, or comparing identity.
  • `readonly(x)` gives a deep read-only proxy that warns on mutation.
  • `customRef((track, trigger) => ({ get, set }))` builds a ref with custom timing, such as a debounced ref.
  • `effectScope()` groups effects created inside scope.run(() => ...) so scope.stop() disposes them all at once. It is how libraries create shared state outside components.

Identity gotcha: reactive(obj) !== obj. Mutating the raw object bypasses tracking, and storing the raw object in a Set then searching for the proxy fails. Always work through the proxy (or consistently through the raw object with markRaw).

Example

import { ref, shallowRef, triggerRef, markRaw, reactive, toRaw, customRef, effectScope, watchEffect } from 'vue'

const rows = shallowRef(Array.from({ length: 50_000 }, (_, i) => ({ id: i })))
rows.value[0].id = -1         // no update (not tracked deeply)
triggerRef(rows)              // force dependents to update
rows.value = rows.value.slice(0, 10)   // assignment is always tracked

const state = reactive({ map: markRaw(new Map()) })   // never proxied
const original = { a: 1 }
const proxy = reactive(original)
console.log(proxy === original, toRaw(proxy) === original)   // false true

function debouncedRef(value, ms = 300) {
  let t
  return customRef((track, trigger) => ({
    get() { track(); return value },
    set(v) { clearTimeout(t); t = setTimeout(() => { value = v; trigger() }, ms) },
  }))
}

const scope = effectScope()
scope.run(() => watchEffect(() => console.log(rows.value.length)))   // 10
scope.stop()                  // disposes every effect created in run()

Remember

  • shallowRef + replacement for large or external data; triggerRef after in-place edits
  • markRaw keeps class instances and library objects out of the proxy system
  • effectScope collects effects so they can be stopped together

Watch out for

  • Comparing a reactive proxy with the raw object by identity (===), or mixing both in Sets and Maps.

Check yourself

  1. You store a 3D-engine scene object in a ref and the app becomes slow. What is the most likely fix?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: vuejs.org/api/reactivity-advanced.html