Explain why destructuring or passing properties out of reactive state can break Vue reactivity, how Proxy tracking works, and when to use toRefs(), toRef(), prop getters, or ref() to avoid stale values.
Frontend interview answer
Why does destructuring break reactivity in Vue? Explain toRefs, toRef, and how to safely extract reactive state
Interview quick answer
Interview focus
This Vue interview question tests whether you can explain Vue Destructuring Breaks Reactivity, connect it to production trade-offs, and handle common follow-up questions.
- Vue Destructuring Breaks Reactivity explanation without falling back to memorized definitions
- Composition API and Reactivity reasoning, edge cases, and production failure modes
- How you would answer the most likely Vue interview follow-up
Use this Vue interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
Full interview answer
Quick answer
In Vue 3, reactive() returns a Proxy, and Vue tracks reads/writes through that proxy's property access. const { count } = state reads state.count once and stores the current value in a local binding, so later reads of count no longer go through the proxy. Use toRefs(state) when you need to destructure or return many fields, toRef(state, 'count') for one field or an optional field, and toRef(() => props.foo) when a composable needs a live readonly prop value. Passing the ref object keeps the connection; destructuring .value just copies the current value.
1. Why destructuring breaks Vue reactivity
Vue cannot track ordinary local variables. It can track a reactive() object because reads call the proxy's get trap and writes call the proxy's set trap. If you stop reading or writing through the proxy, Vue has no dependency to track.
2. The classic stale-value bug
This looks harmless, but count becomes a plain number copy:
import { reactive } from 'vue';
const state = reactive({ count: 0 });
// ❌ Trap: destructuring pulls out the current value
const { count } = state;
state.count++; // source updates
console.log(count); // still 0 (stale local copy)
Important nuance: the disconnect is about the local binding. Primitive properties become plain stale values. If the extracted value is an object that is already reactive, nested mutations may still be reactive, but assigning or reading the local binding itself is no longer the same as reading through state.someKey. In interviews, say the problem precisely: destructuring disconnects that property binding from the source proxy.
3. Vue toRefs vs toReftoRefs() converts each enumerable property on a reactive object into a ref linked to the original source. Use it when returning or destructuring many fields from the same state object.
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Ada' });
// ✅ Each field is a ref linked to state
const { count, name } = toRefs(state);
count.value++; // updates state.count
state.count++; // updates count.value
name.value = 'Lin'; // updates state.name
4. Use toRef() for one field, especially optional fieldstoRef() creates one linked ref. It is cleaner when you only need one property, and it can create a usable ref even when the property does not exist yet. toRefs() only creates refs for properties that are enumerable at the time you call it.
import { reactive, toRef } from 'vue';
const state = reactive({ count: 0 });
const count = toRef(state, 'count');
const status = toRef(state, 'status'); // works even before state.status exists
count.value++; // updates state.count
status.value = 'open'; // creates/updates state.status
5. Common stale value bug: passing a property value
The same bug happens without destructuring. If you pass state.count into a composable, you pass the current number, not a live reactive source.
import { computed, reactive } from 'vue';
function useDouble(countRef) {
return computed(() => countRef.value * 2);
}
const state = reactive({ count: 1 });
// ❌ state.count is just the number 1 here
const brokenDouble = useDouble(state.count);
state.count = 2;
console.log(brokenDouble.value); // NaN or stale, depending on the composable
import { computed, reactive, toRef } from 'vue';
function useDouble(countRef) {
return computed(() => countRef.value * 2);
}
const state = reactive({ count: 1 });
// ✅ Pass a ref linked to state.count
const double = useDouble(toRef(state, 'count'));
state.count = 2;
console.log(double.value); // 4
6. Props and composables: use a live prop source
Props are reactive, but props.foo is still the current property value when you pass it as an argument. For a composable input, prefer the Vue 3.3+ getter form toRef(() => props.foo) for a single readonly prop, or toRefs(props) when you need several prop refs. Version nuance: Vue 3.5+ makes variables destructured directly from defineProps() reactive inside the same <script setup> block, but that compiler feature does not make arbitrary plain values passed to composables reactive.
<script setup>
import { toRef, toRefs } from 'vue';
const props = defineProps({
userId: String,
sort: String,
});
// ❌ Plain current value, not a live composable input
useUser(props.userId);
// ✅ Single readonly prop source (Vue 3.3+)
useUser(toRef(() => props.userId));
// ✅ Several prop refs
const { userId, sort } = toRefs(props);
useSearch(userId, sort);
</script>
7. How this relates to ref()
A ref is safe to pass around as an object because reactivity lives behind its .value getter/setter. But destructuring .value is not safe; it copies the current value exactly like destructuring a reactive primitive property.
import { ref } from 'vue';
const count = ref(0);
const liveCount = count; // ✅ pass the ref object
const { value } = count; // ❌ value is just 0 now
count.value++;
console.log(liveCount.value); // 1
console.log(value); // still 0
Need | Use | Why |
|---|---|---|
Return or destructure many fields from one reactive object |
| Keeps each extracted field linked to the source proxy |
Extract one field from reactive state |
| Avoids converting every property |
Track an optional property that may appear later |
|
|
Pass one prop into a composable |
| Keeps a readonly live source without mutating props |
Replace the whole state value later |
| Reactive objects should be mutated in place; refs handle replacement through |
8. Practical rules of thumb
- Do not directly destructure primitive fields from a
reactive()object when they must stay live. - Do not pass
state.fooorprops.footo a composable that expects a reactive source. - Use
toRefs()at composable return boundaries when consumers naturally destructure the result. - Use
toRef()for single fields, optional fields, and prop getter inputs. - Pass refs themselves; do not destructure or cache
.valuewhen you need updates.
Related Vue interview practice
Use Vue reactivity interview question for the broader proxy/dependency model, and Vue ref vs reactive when the interviewer asks how to choose the right state shape.
Interview intuition: Vue tracks access through a reactive container - a proxy property, a ref's .value, or a getter-backed source. If you copy the value out, you copy the current snapshot, not the subscription.
Summary
Summary
- Destructuring breaks reactivity when it extracts a plain value from Vue's proxy-tracked state.
toRefs()is best for many linked fields and composable return objects.toRef()is best for one field, optional fields, and live readonly prop sources.ref()is the better shape when the whole value needs to be replaced.- The safest interview answer names the boundary: keep reads/writes going through the proxy, ref, or getter source.
Use this as one explanation rep, then continue with the Vue.js interview questions cluster or a guided prep path.