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
- The Essence of Reactive Data: Proxy Interception + Dependency Collection + Triggering Updates
ref()Syntax for wrapping any value,.valueSyntax for accessing itreactive()wraps objects with automatic deep reactivity- The 5 Key Differences Between Ref and Reactive, and When to Use Each
- shallowRef / shallowReactive Shallow Reactivity
- How to Use the toRef / toRefs Utility Functions
- 5 Common Pitfalls That Cause Responsive Design to Fail
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":
// ❌ The "Broken" Version
let count = 0
function increment() {
count++
console.log(count) // 1, 2, 3... It looks like
}
<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()
<!-- ✅ 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:
- Code Volume: 30 lines of jQuery DOM manipulation → 5 lines of Vue
- Bug rate: 50% (forget to update DOM) → 0% (Vue handles it)
- Cognitive load: "which DOM elements depend on this variable?" → "all of them, automatically"
3. The Principles of Reactivity: Proxy + Dependency Collection
(1) 3-Step Response Process
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
- Proxy Interception: Vue 3 uses ES6 Proxies to intercept get/set operations
- Dependency Collection: Records "who used this data" (track) when the template is rendered
- Trigger an update: Notify "all relevant locations" to re-render when data changes (trigger)
(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
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
<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
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
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
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
// ❌ 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.
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
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
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
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
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
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
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
<<<<<<< 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
<<<<<<< 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:
Count: 0
[+1] [Reset]
→ Click +1 → Count: 1
→ Click Reset → Count: 0
<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:
Count: 0
[+1] [Reset]
▶ Example: 2. Reactive wrapper objects
Output:
User: Alice
Age: 25
[+1 Year]
→ Click +1 Year → Age: 26
<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:
User: Alice
Age: 25
[+1 Year]
▶ Example: 3. Comparison of ref and reactive
Output:
// Console output:
userRef.value.name → 'Bob' (reactive)
userReactive.name → 'Bob' (reactive)
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:
// Both approaches work:
userRef.value.name = 'Bob' // ✅ Reactive
userReactive.name = 'Bob' // ✅ Reactive
// userReactive = reactive({}) // ❌ Loses reactivity
▶ Example: 4. toRefs—Destructuring While Preserving Reactivity
Output:
0 - Alice
[+1]
→ Click +1 → 1 - Alice
<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:
0 - Alice
[+1]
▶ Example: 5. shallowRef + triggerRef
Output:
// bigTable: 100,000 rows loaded
// reloadData() → replaces entire array (triggers update)
// updateRow() → mutates internally + triggerRef (forces update)
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:
// shallowRef: only .value replacement triggers re-render
// triggerRef: manually forces re-render after internal mutation
▶ Example: 6. Quick Reference for 5 Common Mistakes
Output:
// 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
ref or reactive?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..value but a template doesn’t?.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)..value.x to access its properties?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.toRefs(). After const { count, name } = toRefs(state), count and name become refs; both {{ count }} in the template and count.value in JavaScript remain reactive.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.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
- The Core of Vue 3's Reactivity System: Proxy Interception + Dependency Tracking + Update Triggering
ref()Encapsulates any value; requires.valuefor JavaScript access; automatically unwrapped by the templatereactive()Wrapped object (Object/Array), deep response, no .value- Choosing between
refandreactive:refis versatile and recommended;reactiveis suitable for "multi-field state" - 5 Major Pitfalls: Replacing reactive objects / Losing reactivity during destructuring / Using reactive with primitive values / shallowRef not tracking deep mutations / Type confusion
- shallowRef / shallowReactive are used for performance optimization with large objects
- toRefs preserves the response when destructuring; toRef converts a regular value to a ref
📝 Exercises
-
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
-
Advanced Problems (Difficulty: ⭐⭐)
Write a user profile editing form using reactive:
- The "state" field contains three fields:
name,email, andage - 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
- The "state" field contains three fields:
-
Challenge Problem (Difficulty: ⭐⭐⭐)
Implementing a Complete Todo App (Data Layer):
- Wrap the
todosarray withreactive - Each todo includes
{ id, text, done } - Methods provided: addTodo(text) / toggleTodo(id) / removeTodo(id) / clearDone()
- Provide the following computed properties: activeCount (number of uncompleted items) and doneCount (number of completed items)
- Display the complete to-do list + count in the template
Initial Data:
{ id: 1, text: 'Learn Vue 3', done: false } - Wrap the