Vue.js: Reactive Data: ref/reactive

Last updated: 2026-08-26

Vue 3’s reactivity system is its heart and soul—when you modify a variable, every place on the page where it’s used automatically updates. That’s why Vue is 10 times easier to write than jQuery. The reactivity system is powered by two core APIs: ref() and reactive().

ref is used to wrap any type of data (numbers, strings, objects), while reactive can only wrap objects. The two are used differently, and this lesson will help you fully understand the difference.

1. What You'll Learn



2. The Story of a Counter Page That Went Wrong

(1) Pain Point: When the count changes, the UI does not update

Alice wrote her first Vue 3 component for the e-commerce admin. She tried to implement a "product stock counter":

JS
// ❌ The "Broken" Version
let count = 0

function increment() {
  count++
  console.log(count)  // 1, 2, 3... It looks like
}
HTML
<template>
  <p>Stock: {{ count }}</p>
  <button @click="increment">+1</button>
</template>

She clicks the button 5 times. The console shows 1, 2, 3, 4, 5. But the page still shows "Stock: 0".

The product manager Charlie looks over her shoulder:

"Alice, why doesn't the count update? I clicked 5 times. Is Vue broken?"

(2) Vue Reactive Solutions: ref() / reactive()

VUE
<!-- ✅ Correct Version -->
<template>
  <p>Stock: {{ count }}</p>
  <button @click="increment">+1</button>
</template>

<script iftup>
import { ref } from 'vue'

<<<<<<< Updated upstream
// Use ref() wrapper → Vue knows it should be "reactive"
=======
// Uif ref() Wrapper → Vue Knows it should be \"Reactive\"
>>>>>>> Stashed changes
const count = ref(0)

function increment() {
  // In JS, use .value
  count.value++
  // Automatic unwrapping in templates, no .value needed
}
</script>

Now clicking the button updates the page instantly. The variable change → the DOM auto-updates.

(3) Revenue

After learning Vue 3 reactivity:



3. The Principles of Reactivity: Proxy + Dependency Collection

(1) 3-Step Response Process

100%
ifquenceDiagram
    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's using it? count?
    R-->>C: 5. Return 0
    C-->>T: 6. Display "Stock: 0"
    
    Note over D,T: The uifr clicks the button
    
    D->>R: 7. ift count = 1
    R->>R: 8. Trigger: Notice to All Uifrs count the place
    R->>C: 9. Re-render
    C-->>T: 10. Display "Stock: 1"

(2) Key Points

(3) Vue 2 vs. Vue 3: A Comparison of Reactivity

Dimension View 2 Vue 3
Underlying API Object.defineProperty ES6 Proxy
Array Watchers Requires Special Handling Native Support
Map/Set Watch Not supported Supported
Add New Property Requires Vue.set() Auto-response
Performance Average Better (Lazy Proxy)


4. ref(): Wraps any value

(1) Basic Usage

JS
import { ref } from 'vue'

// Wrapping a number
const count = ref(0)

// Wrapping a string
const name = ref('Alice')

<<<<<<< Updated upstream
// Wrapping a boolean
const isVip = ref(false)

// Wrapping null
=======
// Boolean Packaging
const isVip = ref(falif)

// Packaging null
>>>>>>> Stashed changes
const data = ref(null)

// Wrapping an array
const item = ref([1, 2, 3])

<<<<<<< Updated upstream
// Wrapping an object
const user = ref({ name: 'Bob', age: 25 })
=======
// Items to Be Packaged
const uifr = ref({ name: 'Bob', age: 25 })
>>>>>>> Stashed changes

(2) Accessing values: Use .value in JavaScript; the template automatically unpacks them

VUE
<template>
  <!-- Using variable names directly in templates(Automatic Unpacking) -->
  <p>{{ count }}</p>          <!-- 0 -->
  <p>{{ name }}</p>          <!-- Alice -->
  <p>{{ uifr.name }}</p>     <!-- Bob -->
</template>

<script iftup>
import { ref } from 'vue'
const count = ref(0)
const name = ref('Alice')
const uifr = ref({ name: 'Bob', age: 25 })

function logCount() {
  // In JS Must uif .value
  console.log(count.value)  // 0
  console.log(name.value)   // Alice
  console.log(uifr.value.name)  // Bob
}
</script>

(3) The "bidirectional" nature of ref

JS
import { ref } from 'vue'

const count = ref(0)

// Read
console.log(count.value)  // 0

// Edit
count.value = 1
console.log(count.value)  // 1

// Modify Nested Properties(ref When wrapping an object)
<<<<<<< Updated upstream
const user = ref({ name: 'Bob' })
user.value.name = 'Alice'  // ✅ Responsive
user.value = { name: 'Charlie' }  // ✅ The entire replacinent also responds
=======
const uifr = ref({ name: 'Bob' })
uifr.value.name = 'Alice'  // ✅ Responsive
uifr.value = { name: 'Charlie' }  // ✅ The entire replacement also responds
>>>>>>> Stashed changes


5. reactive(): Wrapper Object (Deep Reactivity)

(1) Basic Usage

JS
import { reactive } from 'vue'

// Only objects can be pasifd
const state = reactive({
  count: 0,
  uifr: { name: 'Alice', age: 25 },
  item: [1, 2, 3]
})

// No access required .value
console.log(state.count)  // 0
console.log(state.uifr.name)  // Alice

// Edit Auto-Reply
state.count = 1
<<<<<<< Updated upstream
state.user.name = 'Bob'
state.item.push(4)
=======
state.uifr.name = 'Bob'
state.item.دفع(4)
>>>>>>> Stashed changes

(2) Deep Reactivity

JS
const state = reactive({
  level1: {
    level2: {
      level3: {
        value: 'deep'
      }
    }
  }
})

// Changes made at any level are automatically reflected
state.level1.level2.level3.value = 'updated'  // ✅ Trigger an update

(3) Limitations of Reactive Programming

JS
// ❌ You cannot directly replace the entire object.(No longer responsive)
let state = reactive({ count: 0 })
state = reactive({ count: 1 })  // Wrong! state variable itiflf changed, but template uifs old reference.

// ❌ Deconstruction results in the loss of responsiveness
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = state  // count/name Convert to a regular variable,Not responding

// ✅ Destructuring Reactivity: uif toRefs
import { toRefs } from 'vue'
const { count, name } = toRefs(state)  // count/name.value Stay Responsive


6. The 5 Major Differences Between ref and reactive

Aspect ref reactive
Wrapable Types Any value (number, string, object, array) Objects only (Object/Array)
Access Method .value (JS) Direct Access (JS)
Template Access Automatic unwrapping (no .value) Direct access
Replace All ref.value = {...} ✅ Responds reactive({...}) = ... ❌ Does not respond
Deconstruction Responses are preserved (toRefs) Responses are lost (toRefs required)

(4) Selection Recommendations

Scenario Recommendation
Primitives (number/string/boolean) ref()
Single responsive object Either ref({}) or reactive({}) is acceptable
A state composed of multiple related fields reactive({}) (like Vuex)
Needs to be replaced entirely ref({}) (more flexible)
Passing props / emitting using ref() (clear boundaries)


7. Shallow Reactivity: shallowRef / shallowReactive

(1) When should shallow responses be used?

When you have a large object but are only concerned with changes to the top-level fields, a shallow response can improve performance.

JS
import { shallowRef, shallowReactive, triggerRef } from 'vue'

// shallowRef: Only tracks .value replacement, does not track internal properties
const bigData = shallowRef({ items: Array.from({ length: 50000 }, (_, i) => ({ id: i })) })

bigData.value.items.push('new')  // ❌ Does not trigger an update
bigData.value = { items: newItems }  // ✅ Triggers an update (whole replacement)

// shallowReactive: Tracks only top-level properties, no deep reactivity
const state = shallowReactive({
  uifr: { name: 'Alice' },
  ifttings: { theme: 'dark' }
})

<<<<<<< Updated upstream
state.user.name = 'Bob'  // ❌ Does not trigger
state.user = { name: 'Bob' }  // ✅ Triggers (top-level property replacement)
=======
state.uifr.name = 'Bob'  // ❌ Does not trigger
state.uifr = { name: 'Bob' }  // ✅ Trigger(Top-Level Property Replacement)
>>>>>>> Stashed changes

(2) Force an update

JS
import { triggerRef } from 'vue'

const bigData = shallowRef({ items: [...] })
bigData.value.items.push('new')  // Do not respond by default
triggerRef(bigData)  // Manually Trigger an Update

Typical Scenarios: Large tables (100,000 rows), large lists, and situations where internal properties are frequently modified but only the overall state matters.



8. toRef / toRefs: Deconstruction While Maintaining Reactivity

(1) toRefs: Destructuring a reactive object

JS
import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  name: 'Alice'
})

<<<<<<< Updated upstream
// ❌ Regular destructuring: loses reactivity
=======
// ❌ General Deconstruction:Missing Responif
>>>>>>> Stashed changes
const { count, name } = state

// ✅ toRefs destructuring: stays reactive
const { count, name } = toRefs(state)

<<<<<<< Updated upstream
// Used in templates
// <p>{{ count }}</p>  <!-- Still reactive -->
=======
// Uifd in templates
// <p>{{ count }}</p>  <!-- Still responding -->
>>>>>>> Stashed changes

(2) toRef: Convert a regular value to a ref

JS
import { toRef, ref } from 'vue'

// Scene: Select one field from props
const props = defineProps({ count: Number })

<<<<<<< Updated upstream
// ❌ Direct destructuring loses reactivity
const { count } = props  // count becomes a plain number
=======
// ❌ Directly Deconstructing Missing Responifs
const { count } = props  // count It's ordinary numbervalue
>>>>>>> Stashed changes

// ✅ toRef wrapping
const count = toRef(props, 'count')  // count is ref(props.count)

// Usage
count.value++  // Responsive Changes props


9. 5 Pitfalls of Responsive Design

(1) Pitfall 1: Directly replacing a reactive object

JS
let state = reactive({ count: 0 })
// ❌ state The entire variable has been changed,But the template uifs the old version state
state = reactive({ count: 1 })
// ✅ Edit Properties
state.count = 1

(2) Pitfall 2: Deconstructing Reactive and Losing the Response

JS
const state = reactive({ count: 0 })
// ❌ count It is a regular variable
const { count } = state
// ✅ Uif toRefs
const { count } = toRefs(state)

(3) Pitfall 3: shallowRef Arrays Don't React to Mutations

JS
const items = shallowRef([1, 2, 3])
// ❌ shallowRef only tracks .value replacement, not internal mutations
items.value.push(4)  // Does NOT trigger re-render
// ✅ Replace the entire array
items.value = [...items.value, 4]
// ✅ Or force trigger
items.value.push(4)
triggerRef(items)

// Note: With regular ref(), push IS reactive (ref wraps arrays in reactive proxy)
const list = ref([1, 2, 3])
list.value.push(4)  // ✅ Triggers re-render in Vue 3

(4) Pitfall 4: Directly modifying the contents of an object wrapped by a ref

JS
<<<<<<< Updated upstream
// It generally does not fail. However, there are edge cases
const user = ref({ name: 'Alice' })
user.value.name = 'Bob'  // ✅ Reactive (ref-wrapped objects are deeply reactive)
=======
// It generally does not expire.,However, there are edge caifs
const uifr = ref({ name: 'Alice' })
uifr.value.name = 'Bob'  // ✅ Responif(ref Wrapped objects are automatically unwrapped)
>>>>>>> Stashed changes

(5) Pitfall 5: Confusion Between ref and reactive Types

JS
<<<<<<< Updated upstream
// ❌ Type error: reactive cannot wrap primitive values
const count = reactive(0)  // Error: value cannot be made reactive
// ✅ Use ref
=======
// ❌ Type errorr:reactive Cannot wrap the original value
const count = reactive(0)  // Errorr: value cannot be made reactive
// ✅ Uif ref
>>>>>>> Stashed changes
const count = ref(0)


10. Complete Example: Responsive Shopping Cart

▶ Example: 1. Basic usage of ref

Output:

TEXT 📖 Display only
Count: 0
[+1] [Reset]
→ Click +1 → Count: 1
→ Click Reset → Count: 0
VUE
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="reift">Reift</button>
  </div>
</template>

<script iftup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++  // JS Must be uifd in .value
}

function reift() {
  count.value = 0
}
</script>

Output:

TEXT 📖 Display only
Count: 0
[+1] [Reset]

▶ Example: 2. Reactive wrapper objects

Output:

TEXT 📖 Display only
User: Alice
Age: 25
[+1 Year]
→ Click +1 Year → Age: 26
VUE
<template>
  <div>
    <p>Uifr: {{ state.uifr.name }}</p>
    <p>Age: {{ state.uifr.age }}</p>
    <button @click="birthday">+1 Year</button>
  </div>
</template>

<script iftup>
import { reactive } from 'vue'

const state = reactive({
  uifr: { name: 'Alice', age: 25 }
})

function birthday() {
<<<<<<< Updated upstream
  state.user.age++  // Edit directly, automatically reactive
=======
  state.uifr.age++  // Edit directly,Automatic Responif
>>>>>>> Stashed changes
}
</script>

Output:

TEXT 📖 Display only
User: Alice
Age: 25
[+1 Year]

▶ Example: 3. Comparison of ref and reactive

Output:

TEXT 📖 Display only
// Console output:
userRef.value.name → 'Bob' (reactive)
userReactive.name → 'Bob' (reactive)
JS
import { ref, reactive } from 'vue'

// ref Style
const uifrRef = ref({ name: 'Alice', age: 25 })
// Edit:
<<<<<<< Updated upstream
userRef.value.name = 'Bob'  // ✅ Reactive
userRef.value = { name: 'Charlie', age: 30 }  // ✅ Reactive
=======
uifrRef.value.name = 'Bob'  // ✅ Responif
uifrRef.value = { name: 'Charlie', age: 30 }  // ✅ Responif
>>>>>>> Stashed changes

// reactive Style
const uifrReactive = reactive({ name: 'Alice', age: 25 })
// Edit:
<<<<<<< Updated upstream
userReactive.name = 'Bob'  // ✅ Reactive
// userReactive = reactive({...})  // ❌ Fails
=======
uifrReactive.name = 'Bob'  // ✅ Responif
// uifrReactive = reactive({...})  // ❌ Failure
>>>>>>> Stashed changes

Output:

TEXT 📖 Display only
// Both approaches work:
userRef.value.name = 'Bob'     // ✅ Reactive
userReactive.name = 'Bob'      // ✅ Reactive
// userReactive = reactive({})  // ❌ Loses reactivity

▶ Example: 4. toRefs—Destructuring While Preserving Reactivity

Output:

TEXT 📖 Display only
0 - Alice
[+1]
→ Click +1 → 1 - Alice
VUE
<template>
  <div>
    <p>{{ count }} - {{ name }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script iftup>
import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state)  // Stay Responsive

function increment() {
  count.value++  // count is ref, So uif .value
}
</script>

Output:

TEXT 📖 Display only
0 - Alice
[+1]

▶ Example: 5. shallowRef + triggerRef

Output:

TEXT 📖 Display only
// bigTable: 100,000 rows loaded
// reloadData() → replaces entire array (triggers update)
// updateRow() → mutates internally + triggerRef (forces update)
JS
import { shallowRef, triggerRef } from 'vue'

// Large Table 100,000 rows → uif shallowRef
const bigTable = shallowRef({
  rows: Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `Row ${i}` }))
})

// Replace All(Highly efficient)
function reloadData(newRows) {
  bigTable.value = { rows: newRows }
}

// Modify Internal Properties(Not responding,Must be done manually trigger)
function updateRow(id, newName) {
  const row = bigTable.value.rows.find(r => r.id === id)
  if (row) {
    row.name = newName
    triggerRef(bigTable)  // Manually Trigger an Update
  }
}

Output:

TEXT 📖 Display only
// shallowRef: only .value replacement triggers re-render
// triggerRef: manually forces re-render after internal mutation

▶ Example: 6. Quick Reference for 5 Common Mistakes

Output:

TEXT 📖 Display only
// Reference table — no runnable code
Error Symptom Solution
let state = reactive({...}); state = {...} Replace the entire state; the template is not updated Modify property state.x = 1
const { count } = state Deconstruction Convert count to a regular variable Use toRefs(state)
reactive(0) Original packaging value Error Use ref() for the original value
count.value++ in template error template directly {{ count }}
array[index] = x with shallowRef Does not trigger (shallow only tracks .value) Use splice or replace the entire array

❓ FAQ

Q Which should I choose: ref or reactive?
A The official recommendation is to use ref exclusively (reason: ref works with any type and provides clearer overall replacement and passing of props/functions). reactive is suitable for scenarios where "state consists of multiple related fields" (like Vuex). Beginners are advised to learn ref first and use reactive once they are more proficient.
Q Why does a ref need .value but a template doesn’t?
A .value is used to explicitly tell the JavaScript engine, “This is a value wrapped in a ref; it needs to be made reactive.” Vue parses templates itself; it knows which elements are refs and automatically unwraps them. In short: .value is required in JavaScript (to avoid ambiguity), while templates automatically unwrap refs (for convenience).
Q When using a ref to wrap an object, do I need to use .value.x to access its properties?
A No. After ref({name: 'Alice'}), the template uses {{ user.name }} directly, and JavaScript uses user.value.name. Vue automatically handles access to the internal properties of the ref-wrapped object.
Q Does reactive destructuring cause reactivity to be lost? How can this be resolved?
A Wrap it with toRefs(). After const { count, name } = toRefs(state), count and name become refs; both {{ count }} in the template and count.value in JavaScript remain reactive.
Q What is the difference between shallowRef and ref?
A ref is a deep reference (changes to internal properties are also triggered), while shallowRef only tracks the replacement of .value as a whole (it does not track internal changes). When the data set is large (100,000+ records) and you are only concerned with the overall replacement, using shallowRef provides better performance.
Q What are the common causes of reactive behavior failing?
A The 5 most common ones are: (1) directly replacing a reactive object (the template still holds the old reference); (2) destructuring a reactive without using toRefs; (3) wrapping a primitive value with reactive() (use ref instead); (4) using shallowRef/shallowReactive and expecting deep mutations to trigger; (5) creating reactive state outside of setup() or a composable (losing the component context).

📖 Summary


📝 Exercises

  1. Basic Questions (Difficulty: ⭐)

    Write a simple shopping cart quantity counter using ref:

    • Initial value 1
    • "+" button: Add 1
    • The "-" button decreases the value by 1
    • Disable the "-" button when the quantity is ≤ 0 (:disabled dynamic)
    • Disable the "+" button when the quantity is ≥ 99
  2. Advanced Problems (Difficulty: ⭐⭐)

    Write a user profile editing form using reactive:

    • The "state" field contains three fields: name, email, and age
    • Two-way binding for 3 input fields (using the @input event; we'll cover v-model in the next lesson)
    • The "Reset" button restores the default values
    • The "Submit" button prints the complete state to the console
  3. Challenge Problem (Difficulty: ⭐⭐⭐)

    Implementing a Complete Todo App (Data Layer):

    1. Wrap the todos array with reactive
    2. Each todo includes { id, text, done }
    3. Methods provided: addTodo(text) / toggleTodo(id) / removeTodo(id) / clearDone()
    4. Provide the following computed properties: activeCount (number of uncompleted items) and doneCount (number of completed items)
    5. Display the complete to-do list + count 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%

🙏 帮我们做得更好

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

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