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
.valueassignment, 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(() => ...)soscope.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;triggerRefafter in-place editsmarkRawkeeps class instances and library objects out of the proxy systemeffectScopecollects 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.