Hello! In our last lesson, you built your first interactive Vue component, a simple counter. You learned how to use ref() to create a reactive piece of state and update it from the template.
Today, we're going to dive deeper into Vue's reactivity system. You know that when you change count.value, the UI updates automatically, but how does that happen? We'll answer that crucial question. We'll also introduce another function, reactive(), for managing state, and compare its use cases and trade-offs against ref(). This lesson is all about moving from knowing what works to understanding why it works, a key goal for you.
By the end of this lesson, you will be able to:
- Manage complex state (objects) using both
ref()andreactive(). - Explain the key differences, advantages, and limitations of
ref()andreactive(). - Describe the core mechanism of Vue's reactivity system (tracking and triggering).
1. Two Ways to Manage State: ref() and reactive()
In the last lesson, we used ref() to hold a single number: const count = ref(0). This works perfectly for primitive values like numbers, strings, and booleans.
But what about more complex data, like an object representing a user?
You can absolutely use ref() for this:
import { ref } from 'vue'
const user = ref({
name: 'Alex',
email: 'alex@example.com'
})
// To change the name, you'd do:
user.value.name = 'Jordan'
Notice you still need .value to get to the object, and then you access its properties.
Vue provides another way that's designed specifically for objects and arrays: reactive().
import { reactive } from 'vue'
const user = reactive({
name: 'Alex',
email: 'alex@example.com'
})
// To change the name, you'd do:
user.name = 'Jordan'
As you can see, reactive() feels more natural when working with objects because you don't need to use .value in your script.
This image provides a great mental shortcut for their primary use cases:

So, if reactive() is cleaner for objects, why not always use it for them? As you'll see, there are important trade-offs.
2. ref() vs. reactive(): A Detailed Comparison
Choosing between ref() and reactive() involves understanding their limitations. A quick video is the best way to see a direct comparison in action.
Vue 3 Reactive Data | Ref Vs Reactive in 10 Minutes! (ish)
This video from John Komarnicki provides a concise and clear comparison between ref() and reactive(), highlighting the key differences and limitations that will inform which one you should choose.
Watch the video from 03:32 to the end (12:44). The presenter will first demonstrate ref() with an object, then introduce reactive(), and finally, discuss the important limitations of reactive() that make ref() a more versatile and often safer choice.
After watching, let's summarize the key takeaways:
| Feature | ref(value) |
reactive(object) |
|---|---|---|
| Value Types | Can hold any value type (primitives, objects, arrays). | Only works for object types (objects, arrays, Maps, etc.). |
| Script Access | Requires .value to access/mutate the underlying value. (e.g., myRef.value) |
Access properties directly. (e.g., myReactive.prop) |
| Reassignment | You can replace the entire value. count.value = 10 or user.value = { ... } |
You cannot replace the entire object, as this breaks the reactive connection. |
| Destructuring | Behaves predictably. You are working with the ref object itself. | Destructuring primitive properties from the object will break reactivity. |
Best Practice and Recommendation:
Due to the limitations of reactive() (especially regarding reassignment and destructuring), the official Vue documentation and many community experts recommend using ref() as your default choice for all reactive state.
It's more consistent: you always use .value in the script, regardless of whether you're storing a primitive or an object. This consistency reduces cognitive load and prevents subtle bugs.
3. The "Why": How Does Reactivity Actually Work?
Now for the main event. Let's peel back the curtain on Vue's "magic." The entire system is built on a simple concept: track and trigger.
- Track: When your component's template is rendered, Vue tracks which reactive variables it "touches" or reads. It builds a list of dependencies for each variable.
- Trigger: When you modify a reactive variable, Vue finds its dependency list and triggers a re-render for all the components that depend on it.
But standard JavaScript variables don't have a built-in way to know when they are being read from or written to. This is why Vue needs ref() and reactive(). They create special wrappers around your data to enable this tracking.
How ref() Works: Getters and Setters
When you create a ref, Vue wraps your value in an object with a .value property. This isn't just a simple property; it's a JavaScript getter/setter.
Conceptually, a ref looks like this:
// Pseudo-code, not the actual implementation
const myRef = {
_value: 0,
// This function runs when you READ myRef.value
get value() {
track() // Tell Vue "the code reading this depends on me"
return this._value
},
// This function runs when you WRITE to myRef.value
set value(newValue) {
this._value = newValue
trigger() // Tell Vue "my value changed, update everything that depends on me"
}
}
This is why you must use .value in your script. Accessing .value is what allows Vue to perform the tracking and triggering.
How reactive() Works: Proxies
For objects, Vue uses a modern JavaScript feature called a Proxy. A Proxy wraps an object and lets you intercept operations on it, like getting or setting a property.

So when you do state.count++, the Proxy intercepts the get for state.count, runs track(), then intercepts the set, updates the value, and runs trigger(). This is why you don't need .value—the proxy handles interception for every property on the object.
To get the official explanation and solidify these concepts, the Vue documentation is the best place to go.
Reactivity Fundamentals - Vue.js
The official Vue documentation provides a definitive explanation of its reactivity system. Reading these sections will cement your understanding of the concepts we've just discussed.
Please read the following three sections from the official Vue docs: 'Why Refs?': This explains the track/trigger mechanism and why the .value property is necessary. 'reactive()': This section introduces reactive and explains that it uses JavaScript Proxies. 'Limitations of reactive()': This is a very important section that details the pitfalls of reactive (which we discussed earlier), reinforcing why ref is often the preferred choice.
4. Hands-on Practice
Let's apply this knowledge. Clear out your src/App.vue file and we'll build a small user profile editor.
Task: Build a Profile Editor
Create a simple form that displays a user's name and subscription status, and allows you to update them.
Here is the complete code. Copy and paste this into your App.vue.
<script setup>
import { ref } from 'vue'
const user = ref({
name: 'John Doe',
subscribed: true
})
function toggleSubscription() {
user.value.subscribed = !user.value.subscribed
}
// A bonus function to test reassignment
function resetUser() {
user.value = {
name: 'Jane Doe',
subscribed: false
}
}
</script>
<template>
<div class="profile">
<h2>User Profile</h2>
<p>
<strong>Name:</strong>
<input type="text" v-model="user.name" />
</p>
<p>
<strong>Status:</strong>
{{ user.subscribed ? 'Subscribed' : 'Not Subscribed' }}
</p>
<button @click="toggleSubscription">
Toggle Subscription
</button>
<button @click="resetUser">
Reset User
</button>
</div>
</template>
<style scoped>
.profile {
max-width: 400px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
input {
margin-left: 10px;
}
button {
margin-top: 10px;
margin-right: 10px;
}
</style>
Experiment and Reflect:
- Run the app (
npm run dev). Test the input field and the "Toggle Subscription" button. Notice how you have to useuser.valuein the script, but in the template, you can usev-model="user.name"directly. Vue's template unwrapping is at work here. - Click the "Reset User" button. It works perfectly because
refallows full reassignment of its.value. - Your Challenge: Now, refactor the code to use
reactive()instead ofref().- Change
import { ref } from 'vue'toimport { reactive } from 'vue'. - Change
const user = ref({...})toconst user = reactive({...}). - Remove all instances of
.valuefrom the<script>section. - Now, try clicking the "Reset User" button. What happens? Check the browser console. You'll see an error, because you cannot reassign a
reactivevariable. This demonstrates its biggest limitation firsthand.
- Change
Conclusion
Great job! You've gone from simply using reactivity to understanding its fundamental mechanics. This knowledge is what separates a novice from an expert and will help you debug issues with confidence.
Key Takeaways:
ref()is a general-purpose tool for creating reactive state for any data type. It's consistent and robust.reactive()is a specialized tool for making objects reactive, offering cleaner syntax in scripts but with critical limitations (no primitives, no reassignment).- For consistency and to avoid bugs, prefer
ref()as your default. - The reactivity system works by tracking dependencies when data is read (via getters or proxies) and triggering updates when data is written (via setters or proxies).
In our next lesson, we will put this state management to good use. We'll explore some of Vue's most powerful built-in tools, called directives, like v-if to show or hide elements conditionally and v-for to render lists of data from an array.