ref vs reactive in Vue: what’s the real difference, when should you use each, and what are the common reactivity traps?

MediumIntermediateVue
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

Explain the real difference between ref() and reactive() in Vue 3’s Composition API, when each is the better choice (primitives vs objects, replacement vs mutation, destructuring, template unwrapping), and the most common traps that lead to “why isn’t this updating?” bugs.

Answer

Overview

ref() wraps a value in an object with a .value property, while reactive() returns a Proxy of an object. The practical difference is: use ref for single values (especially primitives) and for values you’ll replace; use reactive for “state objects” you mostly mutate. Most bugs come from destructuring, replacing reactive objects, and losing reactivity by pulling values out of proxies.

Concept

ref()

reactive()

Works with primitives

✅ Yes (recommended)

🚫 No (expects object)

Return value

{ value: ... } wrapper

Proxy of the object

Update style

Replace via .value = ... or mutate nested objects

Mutate properties (e.g. state.count++)

Replacement

✅ Easy (just set .value)

⚠️ Reassigning loses reactivity if you overwrite the variable

Template usage

Auto-unwraps .value in templates

Used directly

High-level differences between ref() and reactive()

1. ref(): best for primitives and replaceable values

If the thing is a single value (number, string, boolean) or something you regularly replace (API result object, selected item, current tab), ref keeps it predictable.

JAVASCRIPT
import { ref } from 'vue';

const count = ref(0);
count.value++;

const user = ref(null);
user.value = { id: 1, name: 'Ada' }; // easy replacement
                  

2. reactive(): best for state objects you mutate

If you naturally think in “state object with fields” and you mostly mutate properties, reactive is ergonomic.

JAVASCRIPT
import { reactive } from 'vue';

const state = reactive({
  count: 0,
  filters: { q: '', sort: 'asc' }
});

state.count++;
state.filters.q = 'vue';
                  

3. Trap #1: destructuring reactive breaks reactivity

This is the #1 “why isn’t my UI updating?” moment. When you destructure from a reactive object, you pull out plain values and lose tracking.

JAVASCRIPT
import { reactive } from 'vue';

const state = reactive({ count: 0 });

// ❌ Trap: loses reactivity
const { count } = state;

state.count++; // state changes
console.log(count); // still 0 (stale)
                  

Fix: use toRefs() (or toRef()) so destructured fields stay reactive.

JAVASCRIPT
import { reactive, toRefs } from 'vue';

const state = reactive({ count: 0 });
const { count } = toRefs(state);

count.value++;     // updates state.count
state.count++;     // updates count.value
                  

4. Trap #2: replacing a reactive object breaks consumers

With reactive, the Proxy identity matters. If you reassign the variable, anything holding the old proxy won’t see the new object.

JAVASCRIPT
import { reactive } from 'vue';

let state = reactive({ items: [] });

// ❌ Trap: you replaced the proxy reference
state = reactive({ items: [1, 2, 3] });

// Anything that captured the old 'state' won't update as you expect.
                  

Fix options:
• Prefer ref if you need to replace wholesale.
• Or mutate the existing reactive object instead of replacing it.

JAVASCRIPT
// Option A: use ref for replaceable objects
import { ref } from 'vue';
const state = ref({ items: [] });
state.value = { items: [1, 2, 3] };

// Option B: mutate reactive object in place
import { reactive } from 'vue';
const s = reactive({ items: [] });
s.items.splice(0, s.items.length, 1, 2, 3);
                  

5. Trap #3: forgetting .value in script (but not in templates)

Templates auto-unwrap refs, but JavaScript does not. People mix the two mental models and end up comparing or assigning the wrapper object.

HTML
<script setup>
import { ref } from 'vue';
const count = ref(0);

// ❌ Trap
// if (count > 0) {}

// ✅ Correct
if (count.value > 0) {}
</script>

<template>
  <!-- ✅ Auto-unwrap in templates -->
  <p>{{ count }}</p>
</template>
                  

6. Trap #4: reactive + arrays, maps, and sets (mutation is fine, identity matters)

Vue tracks mutations like push, splice, and property sets. The trap is when you expect reactivity after pulling values out and working with raw copies.

JAVASCRIPT
import { reactive } from 'vue';

const state = reactive({ items: [1, 2] });
state.items.push(3); // ✅ reactive

const itemsCopy = state.items.slice();
itemsCopy.push(4);   // 🚫 not reactive (it's a copy)
                  

7. Practical decision rules

      • Use ref for primitives, DOM refs, and anything you replace (API results, selected entity, current step).
      • Use reactive for cohesive state objects you mutate (form state, filter state).
      • If you want to destructure properties, use toRefs().
      • If you keep wanting to replace the whole state object, you probably want ref, not reactive.

Think of it like this: ref is a box (you replace what’s inside), reactive is a smart object (you mutate fields). Most reactivity traps are basically “I took the value out and expected the magic to follow me.”

Summary

Summary

      • ref() wraps a value and updates via .value (best for primitives + replacements).
      • reactive() returns a Proxy and updates via property mutation (best for state objects).
      • Common traps: destructuring reactive, reassigning reactive objects, forgetting .value in script, and mutating copies instead of the reactive source.

Similar questions
Guides
10 / 34