You have used ref and reactive to declare state. This lesson explains how Vue tracks changes, why a ref needs .value, when reactive silently stops working, and which utilities (toRefs, shallowRef, readonly, toRaw) solve real problems. Understanding these rules is what separates code that "usually works" from code that is reliably reactive.
Vue 3 reactivity is built on JavaScript Proxy. When you call reactive(obj), Vue returns a proxy that intercepts every property read and write. During rendering, or while a computed or watch runs, each property read is recorded as a dependency of that effect. A later write to the same property re-runs exactly the effects that depend on it, and nothing else.
Primitives (numbers, strings, booleans) cannot be proxied, which is why ref exists: it wraps the value in an object with a reactive .value property. The proxy is not the original object: reactive(obj) === obj is false.
| | ref() | reactive() |
| --- | --- | --- |
| Accepts | Any value | Objects, arrays, Map, Set |
| Access in script | .value | Direct property access |
| Access in template | Auto-unwrapped | Direct |
| Replace the whole value | state.value = newObj works | Reassigning breaks the link |
| Destructuring | Keeps the ref intact | Loses reactivity |
ref holding an object converts that object with reactive internally, so user.value.name = 'Ada' is tracked. The official recommendation is to use ref as the default and reach for reactive only when you want a group of related fields without .value.
import { reactive, toRefs, toRef } from 'vue'
const state = reactive({ count: 0, label: 'clicks' })
// 1. Destructuring copies plain values
let { count } = state
count++ // state.count is still 0
// 2. Reassignment replaces the proxy with a plain object
// state = reactive({ count: 5 }) // other code still holds the old proxy
// Fix: convert properties to refs that stay linked to the proxy
const { count: countRef, label } = toRefs(state)
countRef.value++ // state.count is now 1
const single = toRef(state, 'label')toRefs is exactly what makes composables returning reactive objects usable with destructuring: const { x, y } = useMouse(). Passing a reactive property into a function (format(state.count)) also passes a plain number; pass the whole object or a ref instead.
Deep conversion has a cost. For large arrays or objects that you replace wholesale rather than mutate, use shallowRef, which only tracks .value assignment:
import { shallowRef, triggerRef, markRaw } from 'vue'
const rows = shallowRef([])
async function load() {
rows.value = await fetch('/api/rows').then(r => r.json()) // triggers update
}
rows.value.push({ id: 99 }) // NOT tracked; the array is not proxied
triggerRef(rows) // force dependents to re-run if you must mutate
const chart = markRaw(new ChartLibrary()) // never make this reactiveshallowReactive does the same for objects: only top-level properties are reactive. markRaw tells Vue to skip an object entirely, the right choice for third-party class instances such as chart or map objects.
import { reactive, readonly, toRaw, toValue, isRef } from 'vue'
const state = reactive({ theme: 'dark' })
const frozen = readonly(state)
frozen.theme = 'light' // warning in dev, no change
toRaw(state) === state // false: returns the original plain object
toValue(() => 5) // 5 (calls getters; unwraps refs; passes plain values)
isRef(state) // falsereadonly(obj) returns a deep read-only proxy. Use it when you provide state and want children to call functions instead of mutating.toRaw(proxy) returns the original object, useful when a library needs a plain object.unref(x) returns x.value if x is a ref, otherwise x; toValue(x) also calls getters. Both let composables accept refs, getters or plain values.customRef builds a ref with custom get/set, which is how debounced inputs are implemented.count instead of count.value in script; the ref object is always truthy, so if (count) is a silent bug.Why does `const { count } = reactive({ count: 0 })` lose reactivity?
Proxy; ref wraps primitives in an object with a reactive .value.ref by default; reactive is for objects and breaks on destructuring or reassignment.toRefs and toRef convert reactive properties into linked refs.shallowRef, shallowReactive and markRaw reduce the cost of large or third-party objects.readonly, toRaw, unref and toValue cover the remaining edge cases.Next lesson: watch, watchEffect and Side-Effect Cleanup — run code when state changes, control timing, and clean up stale requests.