Frontend interview answer

Why does destructuring break reactivity in Vue? Explain toRefs, toRef, and how to safely extract reactive state

HighIntermediateVue

Interview quick answer

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.

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
Practice more Vue.js interview questions
Interview answer drill

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:

JAVASCRIPT
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 toRef

toRefs() 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.

JAVASCRIPT
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 fields

toRef() 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.

JAVASCRIPT
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.

JAVASCRIPT
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
                  
JAVASCRIPT
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.

VUE
<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.

JAVASCRIPT
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

toRefs(state)

Keeps each extracted field linked to the source proxy

Extract one field from reactive state

toRef(state, 'field')

Avoids converting every property

Track an optional property that may appear later

toRef(state, 'field')

toRefs() only sees enumerable properties present at call time

Pass one prop into a composable

toRef(() => props.foo)

Keeps a readonly live source without mutating props

Replace the whole state value later

ref(initialValue)

Reactive objects should be mutated in place; refs handle replacement through .value

Vue reactivity extraction decision guide

8. Practical rules of thumb

  • Do not directly destructure primitive fields from a reactive() object when they must stay live.
  • Do not pass state.foo or props.foo to 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 .value when 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.

Similar questions
Guides
Preparing for interviews?