Reactivity in Vue 3 Composition API: A Complete Guide to ref() and reactive()

Master Vue 3's reactivity system with the Composition API. Learn when to use ref() vs reactive() with practical code examples and performance best practices.

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.

How Vue Tracks Reactivity 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 operations

The 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()

  1. Only works with objects - You cannot use reactive() with primitives
  2. Cannot replace the entire object - Assigning a new object breaks reactivity
  3. Cannot destructure without losing reactivity - Destructuring a reactive object gives you plain values
ref() vs reactive() Comparison
Aspectref()reactive()
Accepts any valueYes (primitives + objects)Objects only
Access pattern.value in script, unwrapped in templateDirect property access
Deep reactivityOnly with object valuesAlways deep
Replacing entire valuePreserves reactivityBreaks reactivity
Type inferenceStrong with TypeScriptGood but requires annotations
Use casePrimitives, replaceable stateRelated 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
Best Practices Summary

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 .value requirement 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.