ref() reactive()

In the Composition API of Vue 3, ref() and reactive() serve as the two primary gateways for building reactive state. Mastering their definitions, differences, and practical use cases marks the essential first step toward writing effective Vue 3 applications. Powered by ES6 Proxy under the hood, Vue's revamped reactivity system automatically tracks dependencies during rendering and triggers side effects when data changes. Both functions generate trackable state within this same underlying engine, differing only in how they wrap and expose data.
The ref() function accepts any value type—ranging from primitives like strings, numbers, and booleans to complex structures like objects and arrays. It returns a mutable wrapper object featuring a .value property, which must be used to read or modify the underlying state:

import { ref } from 'vue'

const count = ref(0)
const user = ref({ name: 'Alice' })

count.value++            // Triggers an update
user.value.name = 'Bob'  // Nested properties remain reactive

Because JavaScript primitives cannot be directly intercepted by a Proxy, ref() bridges this limitation by wrapping values in an object and intercepting access via .value. When used inside templates, top-level refs are automatically unwrapped, sparing developers from explicitly typing .value.

reactive(): Reactive Objects

In contrast, reactive() is restricted to object types—such as plain objects, arrays, Maps, and Sets—and returns a direct Proxy of the original object. Accessing or modifying properties requires no extra wrappers, preserving native JavaScript syntax:

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  list: ['a', 'b']
})

state.count++
state.list.push('c')

The main advantage of reactive() lies in its natural state organization, making it ideal for grouping related fields into a single state object. However, its limitations are clear: it cannot be reassigned as a whole without losing reactivity, and destructuring its properties severs their reactive connection.

Core Differences at a Glance

Dimension ref() reactive()
Supported Types Any type (primitives & objects) Object types only
Access Syntax Via .value Direct property access
Whole Replacement Supported (by reassigning .value) Not supported
Destructuring Preserves reactivity Loses reactivity (requires toRefs)
Underlying Mechanism Reference wrapper (uses reactive internally for objects) Direct Proxy wrapping

Notably, when ref() wraps an object, it actually leverages reactive() under the hood. They are not opposing forces, but rather two encapsulation granularities built upon the same reactivity architecture.

How to Choose

  • Default to ref(): Officially recommended as the primary choice due to its broad compatibility, uniform semantics, and safety against issues during destructuring or reassignment.
  • Reserve reactive() for Grouped State: Ideal when multiple variables naturally belong together and share the exact same lifecycle, such as a complex form's state object.
  • Maintain Team Consistency: Mixing both approaches haphazardly can introduce cognitive friction regarding .value. Establishing a clear project guideline ensures code readability.

Vue 3 also provides complementary utility functions like shallowRef(), toRef(), toRefs(), and unref() to handle edge cases such as shallow reactivity or maintaining reactivity during destructuring.

Conclusion

ref() and reactive() are the twin pillars of the Vue 3 reactivity system. While ref() unifies primitive and reference handling through a consistent wrapper, reactive() offers a native-feeling object proxy solution. Grasping the principles behind why .value is necessary and why reactive() cannot be reassigned is far more valuable than simply memorizing their differences. In everyday development, adopting ref() as the default choice supplemented by reactive() where appropriate represents the current mainstream community practice.