reactive() and ref()
Understand exactly how Vue's two core reactivity primitives work, and when to reach for each.
On this page 8 sections
How Vue's Reactivity Works
Vue 3's reactivity system is built on native JavaScript Proxies. When a reactive value is read inside a template or a computed/watcher, Vue records that dependency; when the value later changes, Vue knows precisely which parts of the UI depend on it and re-renders only those.
ref()
ref() wraps a value — of any type, including primitives — in a reactive object with a single .value property. You've been using it since the very first lesson.
import { ref } from 'vue';
const count = ref(0);console.log(count.value); // 0
count.value++;console.log(count.value); // 1JavaScript can't make a plain primitive (a number, a string) itself reactive — ref() wraps it in an object so Vue has something it can intercept reads and writes on. Templates automatically unwrap .value for you, which is why you write {{ count }}, not {{ count.value }}, in a template.
reactive()
reactive() wraps an object (not a primitive) in a Proxy directly — no .value needed, since the object's own properties are what become reactive.
import { reactive } from 'vue';
const user = reactive({ name: 'Ada', age: 28 });console.log(user.name); // 'Ada' — no .value needed
user.age++;console.log(user.age); // 29ref vs reactive: Which to Use
| ref() | reactive() | |
|---|---|---|
| Works with primitives? | Yes | No — objects/arrays only |
| Access syntax | .value in <script>, unwrapped in templates | Direct property access, everywhere |
| Can be reassigned entirely? | Yes — myRef.value = newObject | No — reassigning loses reactivity; must mutate properties |
| Common recommendation | Default choice for most values | Fine for a single, cohesive object that's never reassigned wholesale |
Many Vue codebases default to ref() for everything, even objects, purely for consistency — its ability to be reassigned wholesale (myRef.value = newState) is also genuinely useful and something reactive() can't do.
The Destructuring Gotcha
Destructuring a reactive() object's properties breaks their reactive connection — you get a plain, disconnected value instead.
const user = reactive({ name: 'Ada', age: 28 });
// ❌ Breaks reactivity — `age` is now just a disconnected numberconst { age } = user;
// ✅ Use toRefs() to destructure while keeping the reactive connectionimport { toRefs } from 'vue';const { age: reactiveAge } = toRefs(user);Common Beginner Mistakes
Unwrapping only happens automatically in templates — inside <script>, always access and assign through .value on a ref.
user = { name: 'New' } replaces the Proxy with a plain object, losing reactivity — mutate individual properties (user.name = 'New') instead, or use a ref if whole-object reassignment is needed.
FAQs
Yes — ref() actually wraps object values with reactive() internally, so ref({ name: 'Ada' }).value.name is fully reactive as well.
Because JavaScript has no way to intercept reads/writes on a plain variable — wrapping the value in an object with a .value property is what makes interception (and therefore reactivity) possible.
Summary
ref() and reactive() are both built on the same Proxy-based system, with ref() being the more flexible default for most values. Next, you'll look closer at <script setup> itself and the compiler macros it provides.