In Vue 3, why does `const { count } = reactive({ count: 0 })` stop updating, and how does `toRefs()` keep the link?
answer
- destructuring reads once
- a primitive is copied out
- one ref per property
- each ref reads through the proxy
basics
~20 sDestructuring reads state.count once through the proxy and copies the number into a local variable, which Vue cannot track or update. toRefs(state) instead returns an object of refs, one per property, whose .value reads and writes state[key] through the proxy, so the link survives destructuring.
solid answer
~40 sVue 3 tracks reactivity through property access on the proxy. `const { count } = state` performs one `get` on the proxy and assigns the resulting primitive to a local `const`; from then on nothing touches the proxy, so later reads of `count` are untracked and later changes to `state.count` never reach it. `toRefs(state)` returns a plain object whose every property is a ref linked to the source: reading `countRef.value` reads `state.count` through the proxy (tracked), and assigning it writes `state.count` (triggered). So `const { count } = toRefs(state)` gives a destructured `count` that is still live, and templates unwrap it like any top-level ref. `toRefs` covers only keys present when it is called, and in development it warns if handed a plain object.
code
vue · 18 lines<script setup lang="ts">
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, step: 1 })
const { count: frozen } = state // number 0, never updates
const { count, step } = toRefs(state) // live refs
function add() {
state.count += state.step
}
</script>
<template>
<button @click="add">add</button>
<p>live: {{ count }} (step {{ step }})</p>
<p>frozen: {{ frozen }}</p>
</template>go deeper
Recall that destructuring copies primitive values out of a reactive object and that toRefs() turns each property into a linked ref.
Explain that toRefs returns property refs that read and write through the proxy, and why nested objects behave differently from primitives.
Spot disguised copies in reviews - spreads, arguments, ref(state.x) - and know when toRef suits optional keys better than toRefs.
Set conventions for how composables return state and how components consume it, so lost-reactivity bugs are designed out.
## Why destructuring disconnects A `reactive()` object is a proxy: Vue sees each property **read** through its `get` trap and each **write** through its `set` trap. Destructuring is just a sequence of reads: ```ts const state = reactive({ count: 0, user: { name: 'Ada' } }) const { count, user } = state // same as: const count = state.count; const user = state.user ``` After that line: - `count` holds the **number** `0`. It is a plain JavaScript binding with no accessor; reading it later is invisible to Vue, and `state.count++` elsewhere does not change it. - `user` holds the **reactive proxy** of the nested object, because deep reactivity wraps nested objects on access. `user.name = 'Grace'` is still a tracked write; but if someone replaces `state.user` with a new object, `user` keeps pointing at the old one. The docs list this as a limitation of `reactive()`: destructuring a primitive property into a local variable, or passing it into a function, loses the reactivity connection. ## What `toRefs()` returns `toRefs(state)` walks the object's current keys and creates one **property ref** per key: | Access | What happens | |---|---| | `refs.count.value` (read) | reads `state.count` through the proxy, so the reading effect is tracked | | `refs.count.value = 5` (write) | writes `state.count` through the proxy, so subscribers are triggered | | `state.count++` elsewhere | the next read of `refs.count.value` sees the new value | The refs hold no copy of the value; they are pointers to "property `count` of this object". Destructuring the returned plain object copies **refs**, and a ref copied into a local variable is still the same live object: ```ts const { count } = toRefs(state) count.value++ // state.count is now 1 ``` In a template, a top-level ref is unwrapped automatically, so `{{ count }}` renders the number. ## Where it fits 1. **Destructuring local state for readability** without losing updates. 2. **Returning state from a composable** as a plain object of refs, so callers can destructure what they need (the docs recommend returning refs for exactly this reason). 3. **Passing one property into a function** that expects a ref. ## Limits worth knowing - **Only existing keys.** `toRefs()` uses the keys present at call time. A property added later has no ref; use `toRef(state, 'key')`, which works even for a key that does not exist yet. - **Plain objects.** In development, `toRefs()` warns `toRefs() expects a reactive object but received a plain one.` The refs it would return read and write a plain object that nothing tracks. - **Arrays.** `toRefs()` on a reactive array returns an array of refs, one per index at call time. ## The same problem outside destructuring Any operation that copies a primitive out of the proxy has the same effect: `const n = state.count`, spreading `{ ...state }` into a new object, or passing `state.count` as an argument. The spread case is common in code that merges defaults, and it produces a plain snapshot every time. ## Common mistakes - Using `let { count } = state` and incrementing `count`, expecting `state.count` to follow. - Wrapping the copied value: `ref(state.count)` creates an **independent** ref initialised from the number, not a link. - Calling `toRefs()` on a ref instead of on a reactive object; to split a ref holding an object, call it on `ref.value`. ## A quick self-test Predict each result, given `const state = reactive({ n: 1, user: { name: 'Ada' } })`: 1. `const { n } = state; state.n = 2` - `n` is still `1`: a copied number. 2. `const { user } = state; state.user.name = 'Grace'` - `user.name` is `'Grace'`: `user` is the same nested proxy. 3. `const { user } = state; state.user = { name: 'Lin' }` - `user.name` is still `'Ada'`: the local points at the old object. 4. `const { n } = toRefs(state); state.n = 3` - `n.value` is `3`: the property ref reads through the proxy. If all four answers come easily, the model is in place: primitives and references are copied by destructuring, and only refs (or reading through the proxy each time) keep a live link.
- In Vue 3, if you destructure a nested object out of reactive state, is it still reactive?Its contents are: the local variable holds the nested reactive proxy, so writing `user.name` is tracked. But the variable itself is a copy of the reference, so if the parent property is replaced with a new object, the local variable keeps pointing at the old one.
- How does `toRef(state, 'key')` differ from what `toRefs()` gives you for that key?The ref is the same kind - a two-way link to one property. `toRef()` builds just one, and it also works for a key that does not exist yet, which `toRefs()` cannot cover because it only iterates the keys present when it is called.
Destructuring copies today's reading off a meter onto a sticky note; toRefs() hands you a remote display wired to the meter itself, so it always shows the current reading and can even turn the dial.
saying these in an interview costs you the question
- Destructuring a reactive object gives reactive local variables.
- ref(state.count) keeps the new ref in sync with state.count.
- toRefs() copies the current values into new independent refs.
- toRefs() also creates refs for properties added to the object later.
- A nested object destructured from reactive state loses all reactivity.