Understanding Vue's Reactivity System
Vue's reactivity system automatically tracks dependencies and updates the DOM when state changes. In Vue 3, this system is built using JavaScript Proxies, which allow Vue to intercept property access and mutation operations. The Composition API provides two primary ways to create reactive state: ref() for wrapping individual values and reactive() for creating reactive objects.
Understanding how these functions work--and when to use each--is essential for building performant, maintainable applications with our web development services. This guide explores both approaches in depth, comparing their behaviors, use cases, and best practices.
How Reactivity Works Under the Hood
When you use a ref or reactive value in your template, Vue tracks which dependencies are accessed during rendering. Later, when you modify those values, Vue triggers a re-render for any components that are tracking those dependencies.
// Conceptual example of Vue's reactivity tracking
// When you access a ref.value or reactive property:
// 1. Vue's getter intercepts the access
count.value // → triggers track() to record dependency
// 2. Vue's setter intercepts the mutation
count.value++ // → triggers trigger() to notify subscribers
// This is why .value is required in script context -
// Vue needs to intercept read/write operationsThe ref() Function
The ref() function takes an inner value and returns a reactive and mutable ref object. This object contains a single .value property that points to the inner value. Any reads or writes to .value are tracked by Vue's reactivity system, making them ideal for triggering updates throughout your application.
Creating refs with different types:
import { ref } from 'vue'
// Primitive values
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)
// Objects and arrays
const user = ref({ name: 'John', age: 30 })
const items = ref([1, 2, 3])
Accessing and Modifying ref Values
In JavaScript, you must access the value through the .value property:
// Reading the value
console.log(count.value) // 0
// Modifying the value
count.value++
console.log(count.value) // 1
// With objects
user.value.name = 'Jane'
user.value.age = 25
When to Use ref()
Use ref() when you need to work with:
- Primitive values (numbers, strings, booleans)
- Values that might be passed around between functions while maintaining reactivity
- State that could be replaced entirely (not just mutated)
- Template refs for DOM element access
For applications that require seamless data flow across components, choosing the right reactivity approach directly impacts performance and maintainability.
The reactive() Function
The reactive() function returns a reactive proxy of the object you pass to it. Unlike ref(), there's no .value property--you access the object's properties directly. Vue's proxy-based approach intercepts all property access and mutation, making the entire object deeply reactive by default.
import { reactive } from 'vue'
// Creating a reactive object
const state = reactive({
count: 0,
message: 'Hello',
user: {
name: 'John',
age: 30
}
})
// Access properties directly
console.log(state.count) // 0
state.count++
console.log(state.count) // 1
Deep Reactivity
One key difference from ref() is that reactive() applies deep reactivity by default--any nested objects or arrays will also be reactive without needing to wrap them individually.
const state = reactive({
user: {
profile: {
name: 'John'
}
},
items: ['a', 'b', 'c']
})
// All levels are reactive
state.user.profile.name = 'Jane' // Triggers update
state.items.push('d') // Triggers update
Limitations of reactive()
- Only works with objects - You cannot use
reactive()with primitives - Cannot replace the entire object - Assigning a new object breaks reactivity
- Cannot destructure without losing reactivity - Destructuring a reactive object gives you plain values
| Aspect | ref() | reactive() |
|---|---|---|
| Accepts any value | Yes (primitives + objects) | Objects only |
| Access pattern | .value in script, unwrapped in template | Direct property access |
| Deep reactivity | Only with object values | Always deep |
| Replacing entire value | Preserves reactivity | Breaks reactivity |
| Type inference | Strong with TypeScript | Good but requires annotations |
| Use case | Primitives, replaceable state | Related state in an object |
Practical Examples
Example 1: Form Handling with reactive()
import { reactive } from 'vue'
export function useFormValidation() {
const form = reactive({
email: '',
password: '',
errors: {
email: null,
password: null
}
})
function validateEmail() {
form.errors.email = /@/.test(form.email) ? null : 'Invalid email'
}
function validatePassword() {
form.errors.password = form.password.length >= 8
? null
: 'Password must be at least 8 characters'
}
function reset() {
form.email = ''
form.password = ''
form.errors.email = null
form.errors.password = null
}
return { form, validateEmail, validatePassword, reset }
}
Example 2: Async Data with ref()
import { ref } from 'vue'
export function useAsyncData<T>(fetchFn: () => Promise<T>) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
async function load() {
loading.value = true
error.value = null
try {
data.value = await fetchFn() // Clean replacement of data
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
load()
return { data, loading, error, reload: load }
}
These patterns are foundational for building scalable applications that leverage AI automation capabilities and require reliable state management.
Common Pitfalls and How to Avoid Them
Pitfall 1: Forgetting .value with ref
In script sections, forgetting to use .value is the most common mistake:
// ❌ Common mistake
count++ // Doesn't work - count is still a ref
// ✅ Correct
count.value++
Pitfall 2: Destructuring reactive Objects
Destructuring a reactive object breaks reactivity:
const state = reactive({ a: 1, b: 2 })
// ❌ Breaks reactivity
const { a } = state
a = 10 // Won't update state.a
// ✅ Use toRefs() to preserve reactivity
import { toRefs } from 'vue'
const { a, b } = toRefs(state)
a.value = 10 // This works
Pitfall 3: Replacing reactive Objects
const state = reactive({ count: 0 })
// ❌ This breaks reactivity - state is no longer reactive
state = reactive({ count: 10 })
// ✅ Instead, mutate properties
state.count = 10
// ✅ Or use ref for replaceable state
const state = ref({ count: 0 })
state.value = { count: 10 } // Works fine
Key guidelines for using ref() and reactive() effectively
Use ref() for Primitives
ref() is the right choice for numbers, strings, booleans, and when you need to replace entire values.
Use reactive() for Related State
Group related state properties together using reactive() for cleaner code and easier management.
Remember .value in Scripts
Always access ref values through .value in JavaScript, though templates unwrap automatically.
Avoid Destructuring reactive
Use toRefs() if you need to destructure a reactive object while preserving reactivity.
Frequently Asked Questions
Can I mix ref() and reactive()?
Yes! You can use refs inside reactive objects. Vue automatically unwraps refs when accessing them as properties of a reactive object. This combines the benefits of both approaches.
Which is more performant?
Performance is generally similar. Use shallowRef/shallowReactive when you have large nested structures that don't need reactivity to avoid the overhead of deep proxying.
Should I use ref() or reactive() at the component level?
Both work, but many developers prefer ref() at the component level for clearer reactivity tracking. It also makes debugging easier since each ref has a clear identity.
How does Vue know when to re-render?
Vue tracks dependencies during template rendering. When a reactive value changes, Vue's reactivity system triggers a re-render for all components that accessed that value during their last render.
Summary and Recommendations
Both ref() and reactive() are essential tools in the Vue 3 Composition API, and understanding when to use each is key to building efficient applications. Remember these core principles:
- Use
ref()when working with primitives, when you need to replace the entire value, or when you want clear reactivity tracking - Use
reactive()for groups of related state that will be mutated together - Be aware of the
.valuerequirement with refs in script context - Avoid destructuring reactive objects without using
toRefs() - Consider performance implications and use shallow variants when appropriate
By following these guidelines and understanding the underlying reactivity system, you'll be able to write more predictable and performant Vue 3 applications. Proper state management is foundational to any robust web application--our web development team can help you implement these patterns effectively in your projects.