Vue Reactive vs Ref in Vue 3: Key Differences and When to Use Each

In Vue 3, reactive() creates a deeply reactive Proxy of objects and arrays that behaves like normal data, while ref() wraps any value (especially primitives) in a wrapper object with a .value property that tracks changes through the RefImpl class.

Understanding the distinction between vue reactive vs ref is essential for effective state management in the Composition API. Both APIs live in the @vue/reactivity package within the vuejs/core repository and transform static data into reactive state, but they employ fundamentally different architectural patterns suited to distinct use cases.

Core Architectural Differences

Proxy-Based Reactivity with reactive

The reactive() function creates a JavaScript Proxy that intercepts property access and mutations. Located in [packages/reactivity/src/reactive.ts](https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts#L91-L99), this implementation uses createReactiveObject to generate a proxy with handlers like mutableHandlers from [baseHandlers.ts](https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts). Every nested property becomes reactive automatically through deep conversion because nested objects are passed through toReactive, making it ideal for complex object structures.

Wrapper Object Pattern with ref

By contrast, ref() constructs a RefImpl class instance defined in [packages/reactivity/src/ref.ts](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts#L13-L22). This wrapper exposes a single .value property containing the inner value. The class maintains a private _value, a raw _rawValue copy, and a Dep instance for tracking. When you access .value, the getter calls dep.track(), and when you assign to it, the setter invokes dep.trigger() after hasChanged detection (ref.ts#L58-L65).

Automatic Ref Unwrapping Behavior

One critical distinction in the vue reactive vs ref comparison involves how nested references are handled. When you place a ref inside a reactive object, the proxy automatically unwraps the ref during property access (reactive.ts#L79-L81). This means you can read and write state.foo directly rather than state.foo.value. However, ref objects never automatically unwrap nested refs unless explicitly processed with proxyRefs or toRefs.

When to Use Reactive vs Ref

Choose the appropriate API based on your data structure and access patterns:

  • Use reactive when managing complex state shaped as objects or arrays, when you want the value to behave like a normal object without the .value accessor, and when you need automatic deep reactivity and ref unwrapping for nested properties.

  • Use ref when working with single primitive values (strings, numbers, booleans), when exposing reactive values from Composition functions that will be consumed elsewhere (like props or emit), or when you need to replace the entire value object rather than mutating properties.

Practical Code Examples

Complex Object State with reactive

import { reactive, watchEffect } from 'vue'

const state = reactive({ count: 0, nested: { msg: 'hi' } })

watchEffect(() => {
  console.log(`count is ${state.count}, msg is ${state.nested.msg}`)
})

state.count++          // triggers the watcher
state.nested.msg = 'bye' // also triggers because of deep reactivity

Implementation reference: The reactive function creates a proxy that deeply tracks state.count and state.nested.msg through proxy handlers in [reactive.ts](https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts#L91-L99).

Primitive Values with ref

import { ref, watchEffect } from 'vue'

const counter = ref(0)

watchEffect(() => {
  console.log(`counter = ${counter.value}`)
})

counter.value++   // triggers the watcher

Implementation reference: The RefImpl class .value getter tracks dependencies and the setter triggers updates (ref.ts#L58-L65).

Combining Both Patterns

import { reactive, ref } from 'vue'

const state = reactive({
  foo: ref(1),
  bar: { baz: 2 }
})

state.foo = 3   // ref is auto-unwrapped by reactive proxy
state.bar.baz = 5     // deeply reactive

Implementation note: The reactive proxy detects ref properties and automatically unwraps them during access (reactive.ts#L79-L81).

Shallow Variants for Performance Optimization

Both APIs offer shallow alternatives that limit reactivity to the root level only:

  • shallowReactive: Creates a reactive proxy where only root-level property changes are tracked. Nested object mutations do not trigger updates.

  • shallowRef: Wraps the value in a RefImpl but does not recursively convert the inner value to reactive state. Only reassignment of .value triggers effects, not internal property mutations. You can manually force updates using triggerRef.

import { shallowReactive, shallowRef, triggerRef } from 'vue'

const shallowObj = shallowReactive({ a: { x: 1 } })
shallowObj.a.x = 2   // NOT reactive

const shallow = shallowRef({ x: 1 })
shallow.value.x = 2   // NOT reactive
triggerRef(shallow)   // manually trigger watchers

Implementation details: shallowRef sets an IS_SHALLOW flag and triggerRef explicitly calls dep.trigger() (ref.ts#L66-L71, ref.ts#L91-L92).

Summary

Frequently Asked Questions

Can I use reactive with primitive values?

No, reactive only accepts objects and arrays (collections). Attempting to pass a primitive (string, number, boolean) to reactive returns the original value unchanged. For primitive values, ref is the correct choice as it wraps them in an object that can be tracked.

Why does my ref inside reactive lose its .value accessor?

When a ref is nested inside a reactive object, Vue's proxy automatically unwraps it during property access. This is intentional behavior documented in the [reactive.ts](https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts#L79-L81) implementation. You access the value directly as state.property rather than state.property.value.

Which is better for performance: reactive or ref?

For most applications, the performance difference is negligible. However, ref involves slightly less overhead for primitive values since it doesn't create a Proxy. For large objects where you only need to watch specific properties, shallowRef or shallowReactive can improve performance by avoiding deep conversion overhead.

How do I trigger updates in a shallowRef after mutating internal properties?

Since shallowRef only tracks the .value reference, internal mutations don't trigger effects. Import triggerRef from vue and call it with your shallow ref to manually notify dependents: triggerRef(shallowRefVariable). This invokes dep.trigger() directly on the RefImpl instance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →