Suggest an editImprove this articleRefine the answer for “Ref vs reactive in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`ref` vs `reactive`** in Vue.js: `ref` wraps any value in a reactive container accessed via `.value` in script; `reactive` creates a direct proxy of an object with no wrapper. ```js const count = ref(0); count.value++ // .value required in script const state = reactive({ count: 0 }); state.count++ // direct access ``` **Key:** `ref` accepts primitives and allows full reassignment; `reactive` only accepts objects and breaks if you reassign the variable. The Vue team recommends `ref` as the default.Shown above the full answer for quick recall.Answer (EN)Image**`ref` vs `reactive`** in Vue.js: `ref` wraps any value in a reactive container you access via `.value`; `reactive` turns an object into a deep proxy with direct property access and no wrapper. ## Theory ### TL;DR - `ref` = a box around your value; access it via `.value` in script, auto-unwrapped in templates - `reactive` = a live proxy of an object; access properties directly, no wrapper at all - `ref` accepts primitives and objects; `reactive` only works with objects (throws on primitives) - Need to replace the whole value? Only `ref` lets you do that safely - Not sure which to pick? The Vue team now recommends `ref` as the default ### Quick example ```vue <script setup> import { ref, reactive } from 'vue' const count = ref(0) // primitive: needs .value in script count.value++ // count.value is now 1 const state = reactive({ count: 0, user: 'Alice' }) // object: direct access state.count++ // state.count is now 1 </script> <template> <p>{{ count }}</p> <!-- auto-unwrapped: shows 1, no .value --> <p>{{ state.count }}</p> <!-- direct access: shows 1 --> </template> ``` Vue unwraps `ref` in templates automatically. You write `{{ count }}`, not `{{ count.value }}`. In script, you always need `.value`. ### Key difference `ref` adds a thin wrapper around any value. Primitives become reactive through that wrapper, objects get a proxy inside it. The wrapper gives you a stable reference you can reassign. `reactive` skips the wrapper entirely, giving you cleaner dot-notation on objects, but you cannot swap the whole object out. Reassign a `reactive` variable and the proxy is gone. The template goes stale. ### When to use - **Counter, boolean, string** → `ref`. Straightforward and reassignable. - **Form object with nested fields** → `reactive` if you never replace the whole form; `ref` if you reset it with a fresh object. - **API response** → `ref`. The value might be `null` before the request resolves, then an object or array. - **Array of items** → `ref`. You can replace the whole array or `push` into it. - **Unsure?** Many teams use `ref` for everything and reach for `reactive` only when dot-notation genuinely simplifies the code. ### Comparison table | Feature | `ref` | `reactive` | |---|---|---| | Input types | Primitive or object | Object / array only | | Script access | `.value` required | Direct property access | | Template access | Auto-unwrapped | Direct property access | | Full reassignment | Yes (`ref.value = newObj`) | No (breaks the proxy) | | Primitives | Yes (`ref(42)`) | No (throws error) | | Destructuring | Safe (ref stays reactive) | Loses reactivity; use `toRefs` | | TypeScript type | `Ref<T>` | Original type | | Reach for it when | Counters, dynamic values, any primitive | Fixed-shape state, forms with no full reset | ### How Vue handles this internally Both `ref` and `reactive` are built on ES6 `Proxy`. `reactive` wraps the object in a Proxy directly, trapping `get` and `set` at every nested level. `ref` creates a small wrapper object with `get/set` on `.value`, which then plugs into the same proxy system. When you write `{{ count }}` in a template, Vue's compiler calls `unref(count)`: if the value has a `__v_isRef` flag, it reads `.value`; otherwise it returns the value as-is. ### Common mistakes **Using `reactive` on a primitive:** ```js const count = reactive(0) // Error: reactive() must be called on an object ``` Fix: `const count = ref(0)`. **Forgetting `.value` in script:** ```js const count = ref(0) count++ // Wrong: mutates the wrapper reference, not the inner value // count.value stays 0 ``` Fix: `count.value++` or `++count.value`. **Reassigning a `reactive` variable:** ```js let state = reactive({ count: 0 }) state = { count: 10 } // Proxy is gone. Template no longer updates. ``` Fix: mutate properties directly (`state.count = 10`) or switch to `ref`. **Destructuring `reactive` without `toRefs`:** ```js const state = reactive({ name: 'Alice', age: 25 }) let { name } = state // name is now a plain string, not reactive name = 'Bob' // template does not update ``` Fix: `const { name } = toRefs(state)`, then `name.value = 'Bob'`. ### Real-world usage - **Pinia stores**: often use `reactive` for the root state object (fixed shape, direct mutations) - **Nuxt `useFetch`**: returns `data` as a `ref` because the value starts as `null` and resolves later - **VueUse composables**: almost all use `ref` internally (`useStorage`, `useLocalStorage`, and most others) - **VeeValidate / FormKit**: `reactive` for form objects with nested field state - **Component state**: in most codebases I have seen, teams that start mixing both end up standardizing on `ref` after a few months - it handles every case without the reassignment trap ### Follow-up questions **Q:** Why does reassigning a `reactive` variable break reactivity? **A:** Because `reactive()` returns a Proxy tied to the original object. When you write `state = {}`, you drop the reference to that Proxy. Vue's dependency tracker still points at the old proxy, so no updates fire. **Q:** How does the template unwrap `ref` automatically? **A:** Vue's template compiler wraps top-level refs with `unref()` in the generated render function. If the value carries a `__v_isRef` flag, it reads `.value`; otherwise it uses the value directly. **Q:** Can you nest a `ref` inside a `reactive` object? **A:** Yes. `reactive({ count: ref(0) })` works, and Vue auto-unwraps the nested ref so `state.count` works without `.value`. That said, mixing the two adds mental overhead. Picking one system and sticking to it is cleaner. **Q:** What is `shallowRef` vs `shallowReactive`? **A:** Both skip deep reactivity. `shallowRef` only reacts when `.value` is reassigned, not when inner properties change. `shallowReactive` tracks only top-level properties. Both are useful for large objects where deep tracking carries a real cost. **Q:** Is there a performance difference for large objects? **A:** One `reactive` proxy is faster than many individual `ref` wrappers for objects with thousands of properties. The difference is measurable at that scale, but negligible in typical component state. ## Examples ### Basic: counter and form state ```vue <script setup> import { ref, reactive } from 'vue' // ref for a simple counter const count = ref(0) // reactive for a form (the object is only mutated, never replaced) const form = reactive({ name: '', email: '', preferences: { theme: 'light', notifications: true } }) function resetForm() { form.name = '' form.email = '' form.preferences.theme = 'light' // form = {} would drop the proxy here - don't do it } </script> <template> <button @click="count++">Count: {{ count }}</button> <form @submit.prevent> <input v-model="form.name" placeholder="Name" /> <input v-model="form.email" placeholder="Email" /> <label> <input type="checkbox" v-model="form.preferences.notifications" /> Notifications </label> </form> <button @click="resetForm">Reset</button> </template> ``` `count` uses `ref` because it is a number that needs `.value` in script. `form` uses `reactive` because it is an object that gets mutated in place and never replaced whole. ### Edge case: reassignment and reactivity loss This trips developers moving from Vue 2 or mixing both APIs in the same component. ```vue <script setup> import { ref, reactive } from 'vue' const objRef = ref({ count: 0 }) const objReactive = reactive({ count: 0 }) // ref: reassigning .value creates a fresh proxy, reactivity stays intact objRef.value = { count: 10 } console.log(objRef.value.count) // 10 - template updates correctly // reactive: reassigning the variable drops the proxy entirely // objReactive = { count: 10 } // JS runs, but Vue loses track of it // Safe way to update reactive - mutate properties instead Object.assign(objReactive, { count: 10 }) // OK console.log(objReactive.count) // 10 - template still updates </script> ``` `ref` owns the proxy through `.value`. `reactive` IS the proxy. Replace the variable and the proxy disappears.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.