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.

Related Core:reactive() toRefs() ref()

1. What You'll Learn


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":

JS
// ❌ Broken version
let count = 0

function increment() {
  count++
  console.log(count)  // 1, 2, 3... looks right
}
HTML
<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:


3. How Reactivity Works: Proxy + Dependency Tracking

(1) The 3-step flow

100%
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

(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

JS
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

JS
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:

TEXT
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

JS
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

JS
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

JS
// ❌ 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 Yourself

Output:

TEXT
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 Yourself

Output:

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

JS
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

JS
// 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

JS
// 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

JS
// 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 Yourself

Output:

TEXT
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

JS
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

JS
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

JS
// 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

JS
// 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

JS
// ❌ 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 Yourself

Output:

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

Related Core:reactive() toRefs() ref()

❓ FAQ

Q Should I use ref or reactive?
A The official guidance is to use ref for everything (it works with any type and makes replacement, props and function arguments explicit). reactive suits "several related fields forming one state" (Vuex-style). Beginners should learn ref first, then adopt reactive once comfortable.
Q Why does ref need .value in JS but not in templates?
A .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).
Q Does ref-wrapped object access need .value.x?
A Not in templates. After ref({name: 'Alice'}), templates use {{ user.name }} while JS uses user.value.name. Vue handles the inner access for you.
Q Destructuring reactive loses reactivity — how do I fix it?
A Wrap with toRefs(). After const { count, name } = toRefs(state), both are refs: {{ count }} in templates, count.value in JS, fully reactive.
Q What is the difference between shallowRef and ref?
A 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.
Q What commonly breaks reactivity?
A Five cases: (1) replacing a reactive object; (2) destructuring reactive without toRefs; (3) wrapping primitives with reactive; (4) creating refs outside setup; (5) assigning to array indices instead of using splice or replacement.
Q Can reactive wrap Map/Set?
A Yes. Vue 3's Proxy-based reactive supports Map/Set/WeakMap/WeakSet. Vue 2 did not.

📖 Summary


📝 Exercises

  1. 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
  2. Intermediate (difficulty ⭐⭐) Build a user profile form with reactive:

    • state contains name, email and age
    • Three inputs with two-way binding (@input events; v-model comes next lesson)
    • A "Reset" button restores the initial values
    • A "Submit" button logs the whole state to the console
  3. Challenge (difficulty ⭐⭐⭐) Implement the data layer of a complete Todo app:

    1. Wrap a todos array with reactive
    2. Each todo is { id, text, done }
    3. Provide methods: addTodo(text) / toggleTodo(id) / removeTodo(id) / clearDone()
    4. Provide computed values: activeCount and doneCount
    5. Render the full list plus the counters in the template

    Initial data: { id: 1, text: 'Learn Vue 3', done: false }

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏