Because reactive() uses a Proxy to track property access. When you destructure, you extract plain values out of the proxy, and the reactive connection is gone.
ts
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // count is now just the number 0
state.count++
console.log(count) // still 0, reactivity lostThis is especially dangerous when destructuring the return value of a composable:
ts
function useCounter() {
const state = reactive({ count: 0 })
return state
}
const { count } = useCounter() // plain number, not reactiveHow to fix it
Option 1: Use toRefs to convert each property into a ref before destructuring.
ts
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)
state.count++
console.log(count.value) // 1, reactivity preserved (needs .value now)Option 2: Return toRefs() from composables so consumers can destructure safely.
ts
function useCounter() {
const state = reactive({ count: 0 })
return toRefs(state)
}
const { count } = useCounter() // ref, reactivity preservedOption 3: Skip reactive() entirely and use ref() for each value.
ts
const count = ref(0)
const name = ref('Vue')
// No destructuring needed, no gotchasMost teams default to ref() for everything precisely to avoid this pitfall.
See also: How do toRefs, toRef, and toValue work? · Why doesn't reactive() work with primitives?