Vue.js 19 🟢 Reactive Objects and Proxies with reactive()
ref() wraps a value in a reactive object with a .value property. reactive() takes a different approach: it returns a proxy of the object itself, so the object’s properties are accessed directly, without a .value in between. The two functions are the two ways to create reactive state in Vue, and they are not interchangeable. reactive() works only on objects—arrays, maps, sets, and plain objects—and it has behavior that ref() does not: the proxy is the object, the properties are tracked individually, and the reactivity is deep by default.
This chapter covers reactive() in full. You will learn what it returns, how it differs from ref(), how the proxy tracks property access, the limitations that come from the proxy model, and the patterns that make reactive() the right choice—or the wrong one—for a given piece of state.
Key point: reactive(obj) returns a proxy of obj. The proxy intercepts property reads and writes, tracks which components depend on which properties, and triggers re-renders when a tracked property changes. The proxy is not the original object; the original is the target. Accessing the proxy’s properties is the reactive access. The reactivity is deep: nested objects are also proxied. reactive() cannot be used on primitives, and the proxy cannot be reassigned without losing reactivity.
Why reactive() exists
The .value problem. A ref() requires .value in the script. For an object with many properties, state.value.name, state.value.age, state.value.email is verbose. reactive() removes the .value: state.name, state.age, state.email. The object is accessed normally, and the reactivity is transparent.
The deep reactivity problem. A ref() wrapping an object makes the object reactive, but the reactivity is on the .value access. reactive() makes the object itself reactive, and the reactivity is deep—every nested property, every array element, every map entry is tracked. The granularity is finer: a component that reads state.name is not re-rendered when state.age changes.
The proxy problem. JavaScript’s Proxy object is the mechanism. reactive() wraps the target in a proxy that intercepts get and set operations. The proxy is what makes the reactivity work. Understanding the proxy is understanding what reactive() can and cannot do.
The collection problem. reactive() supports Map, Set, WeakMap, and WeakSet. The methods of these collections are intercepted, so map.set(key, value) is reactive, and map.get(key) is tracked. ref() does not support these collections in the same way.
The composition problem. A composable that manages a complex state—a form, a store, a machine—often uses reactive() for the state object. The properties are accessed directly, and the composable returns the object. The component that uses the composable reads and writes the properties, and the reactivity is intact.
a. Creating reactive objects and the proxy model
The reactive() function takes an object and returns a proxy.
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: 'Alice',
items: ['a', 'b', 'c']
})
</script>
<template>
<p>{{ state.count }}</p>
<p>{{ state.name }}</p>
<ul>
<li v-for="item in state.items" :key="item">{{ item }}</li>
</ul>
</template>
The state is a proxy. Accessing state.count goes through the proxy’s get handler, which returns the value and registers the dependency. Writing state.count = 10 goes through the set handler, which updates the value and triggers the re-render.
The reactivity is deep. A nested object is also proxied.
<script setup>
import { reactive } from 'vue'
const state = reactive({
user: {
name: 'Alice',
address: {
city: 'Springfield'
}
}
})
// All of these are reactive
state.user.name = 'Bob'
state.user.address.city = 'Shelbyville'
The state.user is a proxy, and state.user.address is a proxy. The deep proxying is automatic. This is convenient but has a cost: large data structures become expensive to proxy.
The original object is the target. The proxy is a different object.
<script setup>
import { reactive } from 'vue'
const original = { count: 0 }
const state = reactive(original)
console.log(state === original) // false
console.log(state.count) // 0
state.count = 10
console.log(original.count) // 10 (the target is mutated)
The proxy is not the original. Mutating the proxy mutates the target. But the reactivity is on the proxy, not the target. If the original is used directly, the reactivity does not apply.
The proxy is only reactive when accessed through the proxy. Destructuring the reactive object loses the reactivity.
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
// Destructuring loses reactivity
const { count, name } = state
count++ // does not update state.count
The destructured count is a plain number. The proxy is not involved. To keep the reactivity, use toRefs().
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state)
count.value++ // updates state.count
The toRefs creates refs that are linked to the proxy’s properties. The destructured refs are reactive.
b. Limitations of reactive()
The proxy model imposes limits that do not apply to ref().
Primitives. reactive() cannot be used on a primitive.
const count = reactive(0) // error: value cannot be made reactive
The proxy can only wrap an object. For a primitive, use ref().
Reassignment. The proxy cannot be reassigned. Replacing the variable with a new object loses the reactivity.
<script setup>
import { reactive } from 'vue'
let state = reactive({ count: 0 })
state = reactive({ count: 10 }) // the component does not track the new proxy
The let and the reassignment break the connection. The template is compiled to reference the original proxy, and the new proxy is not tracked. Use ref() if the state needs to be reassigned.
Destructuring. As noted, destructuring a reactive object loses the reactivity unless toRefs is used.
Spread. Spreading a reactive object into a new object also loses the reactivity.
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const copy = { ...state } // plain object, no reactivity
The spread reads the properties and copies them into a new plain object. The new object is not a proxy.
Comparison. The proxy is not equal to the original. state === original is false. This can be surprising when the original is used as a key or compared.
Array replacement. Replacing an array in a reactive object is reactive, but the new array is proxied.
<script setup>
import { reactive } from 'vue'
const state = reactive({ items: [1, 2, 3] })
state.items = [4, 5, 6] // reactive
state.items.push(7) // reactive
Both the replacement and the mutation are reactive. The new array is proxied when it is assigned.
Identity. A nested object accessed through the proxy returns a different proxy each time, but Vue caches the proxies, so the identity is stable for the same target.
<script setup>
const state = reactive({ user: { name: 'Alice' } })
console.log(state.user === state.user) // true (cached proxy)
The proxy is cached, so repeated access returns the same proxy.
c. reactive() vs ref() and when to use each
The two functions are the two ways to create reactive state. The choice depends on what the state is and how it is used.
| Aspect | ref() | reactive() |
|---|---|---|
| Value type | Any | Object only |
| Access | .value | Direct |
| Template | Auto-unwrapped | Direct |
| Reassignment | ref.value = new | Not supported |
| Destructuring | Works | Loses reactivity (unless toRefs) |
| Deep reactivity | On .value | Deep by default |
| Collections | Not specifically | Map, Set supported |
| Comparison | ref === ref | proxy !== original |
The general guidance from the Vue documentation is to use ref() for primitives and reactive() for objects that are not reassigned. But the guidance is nuanced. ref() works for objects too, and the .value access is a small cost. Many codebases use ref() exclusively for consistency.
The cases where reactive() is clearly better:
- A form state object with many properties.
state.name,state.email,state.passwordis cleaner thanform.value.name. - A store object that is a singleton and never reassigned.
- A composable that manages a complex state with nested objects and collections.
The cases where ref() is clearly better:
- A primitive:
count,name,loading. - A value that is reassigned:
user,items,data. - A value that is passed to a composable or a child component. The
refis a single object that can be passed and unwrapped. - A value that is used in a template and needs auto-unwrapping.
The mixed pattern is common. A composable uses reactive() for the state object and returns it. The component uses the state directly.
// composables/useForm.js
import { reactive } from 'vue'
export function useForm(initial) {
const form = reactive({ ...initial })
const errors = reactive({})
function reset() {
Object.assign(form, initial)
Object.keys(errors).forEach((k) => delete errors[k])
}
return { form, errors, reset }
}
The component uses the form:
<script setup>
import { useForm } from './composables/useForm'
const { form, errors, reset } = useForm({ name: '', email: '' })
</script>
<template>
<input v-model="form.name">
<p v-if="errors.name">{{ errors.name }}</p>
</template>
The form and errors are reactive proxies. The v-model binds to form.name, and the reactivity is intact. The reset function mutates the proxies in place.
The toRefs pattern is used when the reactive object needs to be destructured.
import { reactive, toRefs } from 'vue'
export function useUser() {
const state = reactive({ name: 'Alice', age: 30 })
return toRefs(state)
}
The component destructures the refs:
<script setup>
import { useUser } from './composables/useUser'
const { name, age } = useUser()
</script>
<template>
<p>{{ name }} is {{ age }}</p>
</template>
The refs are auto-unwrapped in the template. The reactivity is preserved.
A readonly() wrapper can be applied to a reactive object to prevent mutation.
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const publicState = readonly(state)
publicState.count = 10 // warning, no mutation
The readonly proxy intercepts writes and warns in development. The underlying state can still be mutated, but only through the original proxy. This is useful for exposing state to child components without allowing them to modify it.
Complete Example Session
<!-- ============================================ -->
<!-- PART 1: BASIC REACTIVE -->
<!-- ============================================ -->
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
</script>
<template>
<p>{{ state.count }}</p>
<p>{{ state.name }}</p>
</template>
<!-- ============================================ -->
<!-- PART 2: NESTED REACTIVE -->
<!-- ============================================ -->
<script setup>
import { reactive } from 'vue'
const state = reactive({
user: {
name: 'Alice',
address: { city: 'Springfield' }
}
})
function move() {
state.user.address.city = 'Shelbyville'
}
</script>
<template>
<p>{{ state.user.address.city }}</p>
<button @click="move">Move</button>
</template>
<!-- ============================================ -->
<!-- PART 3: ARRAY IN REACTIVE -->
<!-- ============================================ -->
<script setup>
import { reactive } from 'vue'
const state = reactive({ items: ['a', 'b', 'c'] })
function addItem() {
state.items.push('d')
}
</script>
<template>
<ul>
<li v-for="item in state.items" :key="item">{{ item }}</li>
</ul>
<button @click="addItem">Add</button>
</template>
<!-- ============================================ -->
<!-- PART 4: REASSIGNMENT PROBLEM -->
<!-- ============================================ -->
<script setup>
import { reactive } from 'vue'
let state = reactive({ count: 0 })
function reset() {
state = reactive({ count: 0 }) // breaks reactivity
}
</script>
<!-- ============================================ -->
<!-- PART 5: DESTRUCTURING PROBLEM -->
<!-- ============================================ -->
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count } = state // loses reactivity
</script>
<!-- ============================================ -->
<!-- PART 6: TOREFS FIX -->
<!-- ============================================ -->
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state)
</script>
<template>
<p>{{ count }} {{ name }}</p>
</template>
<!-- ============================================ -->
<!-- PART 7: REACTIVE MAP -->
<!-- ============================================ -->
<script setup>
import { reactive } from 'vue'
const state = reactive({ cache: new Map() })
function setValue(key, value) {
state.cache.set(key, value)
}
</script>
<template>
<p>{{ state.cache.get('key') }}</p>
</template>
<!-- ============================================ -->
<!-- PART 8: COMPOSABLE WITH REACTIVE -->
<!-- ============================================ -->
<script setup>
import { useForm } from './composables/useForm'
const { form, errors } = useForm({ name: '', email: '' })
</script>
<template>
<input v-model="form.name">
<input v-model="form.email">
<p v-if="errors.name">{{ errors.name }}</p>
</template>
<!-- ============================================ -->
<!-- PART 9: READONLY -->
<!-- ============================================ -->
<script setup>
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const publicState = readonly(state)
</script>
<template>
<p>{{ publicState.count }}</p>
</template>
<!-- ============================================ -->
<!-- PART 10: REF VS REACTIVE -->
<!-- ============================================ -->
<script setup>
import { ref, reactive } from 'vue'
// Primitive: use ref
const count = ref(0)
// Object: use reactive
const form = reactive({ name: '', email: '' })
// Reassignable object: use ref
const user = ref({ id: 1, name: 'Alice' })
user.value = { id: 2, name: 'Bob' }
</script>
The ten parts covered basic reactive, nested reactive, array in reactive, the reassignment problem, the destructuring problem, the toRefs fix, reactive Map, a composable with reactive, readonly, and the ref-versus-reactive decision.
Quick Reference
reactive() vs ref()
| Aspect | ref() | reactive() |
|---|---|---|
| Value type | Any | Object only |
| Access | .value | Direct |
| Template | Auto-unwrapped | Direct |
| Reassignment | Yes | No |
| Destructuring | Works | Loses reactivity |
| Deep reactivity | On .value | Deep by default |
Map/Set | No | Yes |
isReactive() | No | Yes |
Reactive Functions
| Function | Purpose |
|---|---|
reactive(obj) | Create a reactive proxy |
readonly(obj) | Read-only proxy |
isReactive(obj) | Check if reactive |
isReadonly(obj) | Check if read-only |
toRaw(proxy) | Get the original object |
toRefs(obj) | Object of refs |
markRaw(obj) | Skip proxying |
When to Use
| Situation | Use |
|---|---|
| Primitive | ref |
| Reassignable value | ref |
| Form state object | reactive |
| Nested state | reactive |
| Map/Set | reactive |
| Value passed to composable | ref |
| Singleton store | reactive |
| Destructured state | toRefs(reactive(...)) |
Limitations
| Limitation | Reason |
|---|---|
| No primitives | Proxy needs an object |
| No reassignment | Proxy is the identity |
| Destructuring loses reactivity | Plain values copied |
| Spread loses reactivity | Plain object created |
| Not equal to original | Proxy is a different object |
Best Practices
✅ Do This:
<!-- Use reactive for form state -->
const form = reactive({ name: '', email: '' }) <!-- ✅ -->
<!-- Use reactive for nested state -->
const state = reactive({ user: { name: 'Alice' } }) <!-- ✅ -->
<!-- Use toRefs when destructuring -->
const { name, age } = toRefs(state) <!-- ✅ -->
<!-- Use readonly to expose state safely -->
const publicState = readonly(state) <!-- ✅ -->
<!-- Use ref for reassignable values -->
const user = ref(null)
user.value = { id: 1 } <!-- ✅ -->
<!-- Use ref for primitives -->
const count = ref(0) <!-- ✅ -->
<!-- Use reactive for Map and Set -->
const cache = reactive(new Map()) <!-- ✅ -->
❌ Don’t Do This:
<!-- Don't use reactive on primitives -->
const count = reactive(0) // error <!-- ❌ -->
<!-- Don't reassign a reactive variable -->
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // breaks reactivity <!-- ❌ -->
<!-- Don't destructure without toRefs -->
const { count } = state // loses reactivity <!-- ❌ -->
<!-- Don't spread a reactive object -->
const copy = { ...state } // plain object <!-- ❌ -->
<!-- Don't compare the proxy to the original -->
if (state === original) // false <!-- ❌ -->
<!-- Don't use reactive when you need to reassign -->
const data = reactive({ items: [] })
data = reactive({ items: [1] }) // breaks <!-- ❌ -->
<!-- Don't use reactive for a single value -->
const loading = reactive({ value: false }) // use ref instead <!-- ⚠️ -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Reactivity lost on reassignment | Proxy replaced with a new object | Use ref |
| Destructured state not reactive | Plain values copied | Use toRefs |
| Cannot use on primitive | Proxy needs an object | Use ref |
| Spread loses reactivity | New plain object created | Use toRefs or the proxy |
| Proxy not equal to original | Different objects | Use the proxy consistently |
Map mutations not reactive | Not proxied | Use reactive |
| Slow with large data | Deep proxying | Use shallowReactive |
Real-World Examples
1. Form State
<script setup>
import { reactive } from 'vue'
const form = reactive({ name: '', email: '', message: '' })
</script>
<template>
<input v-model="form.name">
<input v-model="form.email">
<textarea v-model="form.message"></textarea>
</template>
2. Nested State
<script setup>
import { reactive } from 'vue'
const state = reactive({
user: { name: 'Alice', address: { city: 'Springfield' } }
})
</script>
3. Array State
<script setup>
import { reactive } from 'vue'
const state = reactive({ todos: [] })
function addTodo(text) {
state.todos.push({ id: Date.now(), text })
}
</script>
4. Map State
<script setup>
import { reactive } from 'vue'
const state = reactive({ cache: new Map() })
function set(key, value) {
state.cache.set(key, value)
}
</script>
5. Composable
import { reactive } from 'vue'
export function useForm(initial) {
const form = reactive({ ...initial })
function reset() {
Object.assign(form, initial)
}
return { form, reset }
}
6. toRefs
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state)
</script>
<template>
<p>{{ count }} {{ name }}</p>
</template>
7. Readonly
<script setup>
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const publicState = readonly(state)
</script>
<template>
<p>{{ publicState.count }}</p>
</template>
8. toRaw
<script setup>
import { reactive, toRaw } from 'vue'
const state = reactive({ count: 0 })
const raw = toRaw(state)
console.log(raw.count)
</script>
9. markRaw
<script setup>
import { reactive, markRaw } from 'vue'
const someHeavyObject = markRaw(loadLargeData())
const state = reactive({ data: someHeavyObject })
</script>
10. Shallow Reactive
<script setup>
import { shallowReactive } from 'vue'
const state = shallowReactive({
count: 0,
user: { name: 'Alice' }
})
state.count++ // reactive
state.user.name = 'Bob' // not reactive
</script>
Visual
Reactive Proxy
┌─────────────────────────────────────────────────────────────┐
│ const state = reactive({ count: 0 }) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PROXY │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ get(target, 'count') │ │ │
│ │ │ → track dependency │ │ │
│ │ │ → return target.count │ │ │
│ │ │ │ │ │
│ │ │ set(target, 'count', value) │ │ │
│ │ │ → update target.count │ │ │
│ │ │ → trigger re-render │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ TARGET: { count: 0 } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
reactive() vs ref()
┌─────────────────────────────────────────────────────────────┐
│ ref() │
│ │
│ const count = ref(0) │
│ Script: count.value │
│ Template: count │
│ Reassign: count.value = 10 │
│ Type: any │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ reactive() │
│ │
│ const state = reactive({ count: 0 }) │
│ Script: state.count │
│ Template: state.count │
│ Reassign: not supported │
│ Type: object only │
│ │
└─────────────────────────────────────────────────────────────┘
Destructuring Problem
┌─────────────────────────────────────────────────────────────┐
│ WITHOUT toRefs │
│ │
│ const state = reactive({ count: 0, name: 'Alice' }) │
│ const { count, name } = state │
│ │
│ count ──▶ plain number 0 │
│ name ──▶ plain string 'Alice' │
│ │
│ count++ → does not update state.count │
│ The reactivity is lost. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ WITH toRefs │
│ │
│ const { count, name } = toRefs(state) │
│ │
│ count ──▶ ref linked to state.count │
│ name ──▶ ref linked to state.name │
│ │
│ count.value++ → updates state.count │
│ The reactivity is preserved. │
│ │
└─────────────────────────────────────────────────────────────┘
Decision Tree
┌─────────────────────────────────────────────────────────────┐
│ Is the value a primitive? │
│ │ │
│ ├── YES ──▶ ref() │
│ │ │
│ └── NO │
│ │ │
│ ▼ │
│ Will the value be reassigned? │
│ │ │
│ ├── YES ──▶ ref() │
│ │ │
│ └── NO │
│ │ │
│ ▼ │
│ Is the value destructured? │
│ │ │
│ ├── YES ──▶ toRefs(reactive(...)) │
│ │ │
│ └── NO ──▶ reactive() │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Function | reactive(obj) |
| Returns | Proxy of the object |
| Value type | Object, array, Map, Set |
| Access | Direct properties |
| Template | Direct properties |
| Reactivity | Deep by default |
| Reassignment | Not supported |
| Destructuring | Loses reactivity |
| Fix for destructuring | toRefs() |
| Read-only wrapper | readonly() |
| Original object | toRaw() |
| Skip proxying | markRaw() |
| Shallow variant | shallowReactive() |
Key takeaways:
reactive()returns a proxy of the object. The proxy intercepts property reads and writes, tracks dependencies, and triggers re-renders. The reactivity is deep: nested objects, arrays, and collections are proxied.reactive()works only on objects. Primitives must useref(). The proxy needs an object to wrap.- The proxy cannot be reassigned. Replacing the variable with a new object breaks the connection between the template and the state. Use
ref()if the value needs to be reassigned. - Destructuring a reactive object loses the reactivity. The destructured properties are plain values copied from the proxy. Use
toRefs()to create refs linked to the proxy’s properties. - The proxy is not the original object.
reactive(obj) !== obj. The proxy is a different object that wraps the original. Mutations to the proxy mutate the target. reactive()supportsMapandSet. The collection methods are intercepted, somap.set()andset.add()are reactive.ref()does not support these collections in the same way.- Use
readonly()to expose state safely. The read-only proxy prevents mutation from the consumer. UsemarkRaw()to skip proxying for large or complex objects.
Remember: reactive() and ref() are the two ways to create reactive state in Vue. ref() wraps any value in a .value ref. reactive() proxies an object so its properties are reactive directly. The choice depends on the value type, whether it is reassigned, and whether it is destructured. Use ref() for primitives and reassignable values. Use reactive() for form state, nested objects, and collections. Use toRefs() when a reactive object needs to be destructured. And remember that the proxy is the mechanism: it intercepts property access, tracks dependencies, and triggers the re-render. Understand the proxy, and the behavior of reactive() follows.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!