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

> Understand the key differences between Vue reactive and ref in Vue 3. Learn when to use each for optimal reactivity and performance in your applications.

- Repository: [Vue/core](https://github.com/vuejs/core)
- Tags: deep-dive
- Published: 2026-02-16

---

**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)](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/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)](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`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/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`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/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`

```typescript
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/reactive.ts)](https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts#L91-L99).

### Primitive Values with `ref`

```typescript
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`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts#L58-L65)).

### Combining Both Patterns

```typescript
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`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/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`.

```typescript
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`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts#L66-L71), [`ref.ts#L91-L92`](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts#L91-L92)).

## Summary

- **`reactive`** returns a Proxy that deeply converts objects/arrays, automatically unwraps nested refs, and allows direct property access without `.value`.
- **`ref`** returns a `RefImpl` wrapper with a `.value` property, optimized for primitives and single values that need to be replaced entirely.
- Both systems rely on the shared effect system in [[`effect.ts`](https://github.com/vuejs/core/blob/main/effect.ts)](https://github.com/vuejs/core/blob/main/packages/reactivity/src/effect.ts) and dependency tracking via `Dep` in [[`dep.ts`](https://github.com/vuejs/core/blob/main/dep.ts)](https://github.com/vuejs/core/blob/main/packages/reactivity/src/dep.ts).
- Use `shallowReactive` and `shallowRef` when you need reactive change detection only at the root level to improve performance.

## 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/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.