Vue.js: Reactive Data: ref/reactive
Last updated: 2026-10-20
Vue 3's reactivity system is its soul: when you change a variable, every place on the page that uses it updates automatically. That is why Vue is about 10× simpler than hand-written jQuery. Two core APIs power it: ref() and reactive().
ref wraps any type of data (numbers, strings, objects); reactive only wraps objects. Their usage differs, and this lesson makes the difference crystal clear.
1. What You'll Learn
- What reactivity really is: Proxy interception + dependency tracking + update triggering
ref()wrapping any value, and the.valueaccess syntaxreactive()wrapping objects with automatic deep reactivity- The 5 key differences between ref and reactive, and when to pick each
- shallowRef / shallowReactive for shallow reactivity
- How to use the toRef / toRefs helpers
- The 5 common pitfalls that silently break reactivity
2. A Counter Page Gone Wrong
(1) The pain: changing count does not update the UI
Alice writes her first Vue 3 component for an e-commerce dashboard. She tries to build a "product stock counter":
// ❌ Broken version
let count = 0
function increment() {
count++
console.log(count) // 1, 2, 3... looks right
}
<template>
<p>Stock: {{ count }}</p>
<button @click="increment">+1</button>
</template>
She clicks the button 5 times. The console prints 1, 2, 3, 4, 5. But the page still shows "Stock: 0".
Her product manager Charlie glances over her shoulder:
"Alice, why isn't count updating? I clicked 5 times. Is Vue broken?"
(2) The Vue fix: ref() / reactive()
Now clicking updates the page instantly. Change a variable → the DOM updates itself.
(3) The payoff
Once you understand Vue 3 reactivity:
- Code size: 30 lines of jQuery DOM juggling → 5 lines of Vue
- Bug rate: 50% (forgotten DOM updates) → 0% (Vue handles it)
- Mental load: "Which DOM nodes depend on this variable?" → "All of them, automatically"
3. How Reactivity Works: Proxy + Dependency Tracking
(1) The 3-step flow
sequenceDiagram
participant T as Template
participant C as Component
participant R as Reactive<br/>(Proxy)
participant D as Data
T->>C: 1. Render (read count)
C->>R: 2. get count
R->>D: 3. Read 0
R->>R: 4. Track: who uses count?
R-->>C: 5. Return 0
C-->>T: 6. Display "Stock: 0"
Note over D,T: User clicks the button
D->>R: 7. set count = 1
R->>R: 8. Trigger: notify everything using count
R->>C: 9. Re-render
C-->>T: 10. Display "Stock: 1"
(2) Key points
- Proxy interception: Vue 3 uses ES6 Proxy to intercept get/set operations
- Dependency tracking: while rendering, Vue records "who read this data" (track)
- Triggering updates: when data changes, everything that read it re-renders (trigger)
(3) Vue 2 vs Vue 3 reactivity
| Aspect | Vue 2 | Vue 3 |
|---|---|---|
| Underlying API | Object.defineProperty |
ES6 Proxy |
| Array tracking | Needs special handling | Native support |
| Map/Set tracking | Not supported | Supported |
| Adding new properties | Needs Vue.set() |
Reactive automatically |
| Performance | Moderate | Better (lazy proxying) |
4. ref(): Wrapping Any Value
(1) Basic usage
import { ref } from 'vue'
// Wrap a number
const count = ref(0)
// Wrap a string
const name = ref('Alice')
// Wrap a boolean
const isVip = ref(false)
// Wrap null
const data = ref(null)
// Wrap an array
const items = ref([1, 2, 3])
// Wrap an object
const user = ref({ name: 'Bob', age: 25 })
(2) Reading values: .value in JS, auto-unwrapped in templates
(3) ref's two-way nature
import { ref } from 'vue'
const count = ref(0)
// Read
console.log(count.value) // 0
// Write
count.value = 1
console.log(count.value) // 1
// Mutate nested properties (when ref wraps an object)
const user = ref({ name: 'Bob' })
user.value.name = 'Alice' // ✅ reactive
user.value = { name: 'Charlie' } // ✅ replacing the whole object is reactive too
▶ Example: Basic usage of ref
▶ Try it Yourself
Output:
Count: 0 and two buttons. Clicking +1 shows Count: 1;
clicking Reset brings it back to Count: 0.
5. reactive(): Wrapping Objects (Deep Reactivity)
(1) Basic usage
import { reactive } from 'vue'
// Objects only
const state = reactive({
count: 0,
user: { name: 'Alice', age: 25 },
items: [1, 2, 3]
})
// No .value when accessing
console.log(state.count) // 0
console.log(state.user.name) // Alice
// Mutations are reactive
state.count = 1
state.user.name = 'Bob'
state.items.push(4)
(2) Deep reactivity
const state = reactive({
level1: {
level2: {
level3: {
value: 'deep'
}
}
}
})
// Any level is reactive
state.level1.level2.level3.value = 'updated' // ✅ triggers an update
(3) reactive's limitations
// ❌ You cannot replace the whole object (reactivity is lost)
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // WRONG: the variable is rebound, templates keep the old reference
// ❌ Destructuring loses reactivity
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = state // count/name become plain values, no longer reactive
// ✅ Keep reactivity while destructuring: use toRefs
import { toRefs } from 'vue'
const { count, name } = toRefs(state) // count/name stay reactive as refs
▶ Example: Reactive wrapper objects
▶ Try it YourselfOutput:
User: Alice and Age: 25. Each click on +1 Year increments the age (26, 27, ...).
6. ref vs reactive: 5 Key Differences
| Aspect | ref | reactive |
|---|---|---|
| What it wraps | Any value (number, string, object, array) | Objects only (Object/Array) |
| Access in JS | .value |
Direct property access |
| Access in templates | Auto-unwrapped (no .value) | Direct property access |
| Whole replacement | ref.value = {...} ✅ reactive |
reactive({...}) = ... ❌ not reactive |
| Destructuring | Works with toRefs | Loses reactivity (needs toRefs) |
(4) Choosing between them
| Scenario | Recommendation |
|---|---|
| Primitives (number/string/boolean) | ref() |
| A single reactive object | Either ref({}) or reactive({}) |
| Several related fields forming one state | reactive({}) (Vuex-like) |
| You need whole-object replacement | ref({}) (more flexible) |
| Passing through props / emits | ref() (clear boundaries) |
▶ Example: Comparison of ref and reactive
▶ Try it YourselfOutput:
Both blocks start at Alice | 25.
"Update ref" rewrites the ref's object (Bob, then Charlie | 30);
"Update reactive" mutates the object in place (Bob) — replacing the whole variable would break it.
7. Shallow Reactivity: shallowRef / shallowReactive
(1) When to use shallow reactivity
When you hold a large object but only care about top-level changes, shallow reactivity saves performance.
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef: only tracks replacement of .value, not inner mutations
const bigData = shallowRef({ items: [...50000 items] })
bigData.value.items.push('new') // ❌ no update
bigData.value = { items: [newItems] } // ✅ updates (whole replacement)
// shallowReactive: only the first level is tracked
const state = shallowReactive({
user: { name: 'Alice' },
settings: { theme: 'dark' }
})
state.user.name = 'Bob' // ❌ no update
state.user = { name: 'Bob' } // ✅ updates (top-level property replaced)
(2) Forcing an update
// Use the global API
const { triggerRef, shallowRef } = Vue
const bigData = shallowRef({ items: [] })
bigData.value.items.push('new') // not reactive by default
triggerRef(bigData) // trigger an update manually
Typical use cases: huge tables (100,000 rows), big lists, and structures where inner mutations happen often while only the overall state matters.
8. toRef / toRefs: Destructuring Without Losing Reactivity
(1) toRefs: destructuring a reactive object
// Destructure from Vue inside setup()
const { reactive, toRefs } = Vue
const state = reactive({
count: 0,
name: 'Alice'
})
// ❌ Plain destructuring: reactivity lost
const { count, name } = state
// ✅ toRefs destructuring: reactivity preserved
const { count, name } = toRefs(state)
// In templates
// <p>{{ count }}</p> <!-- still reactive -->
(2) toRef: turning a value into a ref
// Destructure from Vue inside setup()
const { toRef, ref } = Vue
// Scenario: take one field out of props
const props = defineProps({ count: Number })
// ❌ Destructuring loses reactivity
const { count } = props // count is a plain number
// ✅ Wrap it with toRef
const count = toRef(props, 'count') // count is ref(props.count)
// Usage
count.value++ // reactive mutation of the prop
▶ Example: toRefs keeps destructuring reactive
▶ Try it YourselfOutput:
0 - Alice;
every click increments the count ref while name stays bound — destructuring through toRefs kept both reactive.
9. 5 Pitfalls That Break Reactivity
(1) Pitfall 1: replacing a reactive object
let state = reactive({ count: 0 })
// ❌ The variable is rebound, templates keep the old object
state = reactive({ count: 1 })
// ✅ Mutate the property instead
state.count = 1
(2) Pitfall 2: destructuring a reactive object
const state = reactive({ count: 0 })
// ❌ count is a plain value
const { count } = state
// ✅ Use toRefs
const { count } = toRefs(state)
(3) Pitfall 3: assigning to array elements
// Inside setup()
const items = ref([1, 2, 3])
// ❌ Direct index assignment may not be detected in older setups
items.value.push(4)
// ✅ Replace the array instead
items.value = [...items.value, 4]
(4) Pitfall 4: mutating a ref-wrapped object directly
// Usually fine, with edge cases
const user = ref({ name: 'Alice' })
user.value.name = 'Bob' // ✅ reactive (ref deep-proxies wrapped objects)
(5) Pitfall 5: confusing ref and reactive
// ❌ Type error: reactive cannot wrap a primitive
const count = reactive(0) // error: value cannot be made reactive
// ✅ Use ref
const count = ref(0)
(6) 📊 Quick reference for the 5 mistakes
| Mistake | Symptom | Fix |
|---|---|---|
let state = reactive({...}); state = {...} |
Whole state replaced, template stops updating | Mutate properties: state.x = 1 |
const { count } = state |
count becomes a plain value | Use toRefs(state) |
reactive(0) on a primitive |
Error | Use ref() for primitives |
count.value++ in a template |
Error | Use {{ count }} in templates |
array[index] = x |
May not react | Use splice or replace the array |
10. Complete Example: Reactive Shopping Cart
Putting the whole lesson together: ref for quantities, reactive for multi-field state, computed for derived totals, toRefs to destructure without losing reactivity.
▶ Example: Reactive shopping cart (ref + reactive + toRefs + computed)
▶ Try it YourselfOutput:
Cart (2 items), two rows with +/- buttons, Total items: 2, Total: $228.
Clicking + updates the row total instantly; "Add" appends a row and both totals follow;
"Reset" restores the initial cart.
❓ FAQ
.value in JS but not in templates?.value makes it explicit to the JavaScript engine that this is a wrapped reactive value. Templates are parsed by Vue itself, which knows which bindings are refs and unwraps them. In short: .value in JS (to avoid ambiguity), automatic unwrapping in templates (for convenience)..value.x?ref({name: 'Alice'}), templates use {{ user.name }} while JS uses user.value.name. Vue handles the inner access for you.toRefs(). After const { count, name } = toRefs(state), both are refs: {{ count }} in templates, count.value in JS, fully reactive.ref is deeply reactive (inner mutations trigger updates). shallowRef only tracks replacement of .value. Use shallowRef for very large data (100k+ rows) where only wholesale replacement matters.📖 Summary
- Vue 3 reactivity core: Proxy interception + dependency tracking (track) + update triggering (trigger)
ref()wraps any value; JS access uses.value, templates auto-unwrapreactive()wraps objects (Object/Array), deeply reactive, no.value- Choosing: ref is universal and recommended; reactive fits multi-field state
- 5 pitfalls: replacing reactive objects / destructuring / reactive on primitives / array index assignment / mixing the two types
- shallowRef / shallowReactive optimise performance for large objects
- toRefs preserves reactivity when destructuring; toRef turns a field into a ref
📝 Exercises
-
Basic (difficulty ⭐) Build a simple shopping-cart quantity counter with ref:
- Start at 1
- "+" adds one
- "-" subtracts one
- Disable "-" when the quantity is ≤ 0 (dynamic
:disabled) - Disable "+" when the quantity is ≥ 99
-
Intermediate (difficulty ⭐⭐) Build a user profile form with reactive:
- state contains
name,emailandage - Three inputs with two-way binding (
@inputevents; v-model comes next lesson) - A "Reset" button restores the initial values
- A "Submit" button logs the whole state to the console
- state contains
-
Challenge (difficulty ⭐⭐⭐) Implement the data layer of a complete Todo app:
- Wrap a todos array with reactive
- Each todo is
{ id, text, done } - Provide methods: addTodo(text) / toggleTodo(id) / removeTodo(id) / clearDone()
- Provide computed values: activeCount and doneCount
- Render the full list plus the counters in the template
Initial data:
{ id: 1, text: 'Learn Vue 3', done: false }