Vue 3 gives you two main functions for creating reactive state in the Composition API: ref() and reactive(). Both wrap values in Vue's Proxy reactivity system, yet they follow different rules for reassignment, data types, and destructuring.
Picking the right primitive prevents silent reactivity bugs where a variable updates in JavaScript while the template stops refreshing.
flowchart TD
Prim["Primitives or Reassignable Objects"] --> Ref["ref(val) -> .value Wrapper"]
Obj["Grouped Form or State Objects"] --> React["reactive(obj) -> ES6 Proxy"]
React -->|Destructured Directly| Break["Loses Reactivity"]
React -->|With toRefs| Safe["Keeps Reactive Links"]
ref() and reactive()| Feature | ref() |
reactive() |
|---|---|---|
| Value Types | Primitives, arrays, and objects | Objects, arrays, Map, Set only |
| Script Access | Uses .value (count.value++) |
Direct properties (state.count++) |
| Template Access | Auto-unwrapped ({{ count }}) |
Direct properties ({{ state.count }}) |
| Full Reassignment | Safe (user.value = nextUser) |
Breaks reactive connection |
Both primitives drive template updates automatically, yet each handles object replacement and destructuring differently. Notice how toRefs(userState) turns each property of a reactive object into a linked ref so destructuring stays reactive.
<script setup>
import { ref, reactive, toRefs } from 'vue';
const count = ref(0);
const userRef = ref({ name: 'Alex' });
const userState = reactive({
name: 'Sarah',
role: 'Developer'
});
function updateRef() {
count.value++;
userRef.value = { name: 'Jordan' };
}
function updateReactive() {
userState.name = 'Taylor';
}
const { name, role } = toRefs(userState);
</script>
<template>
<div class="reactivity-demo">
<p>Ref Count: {{ count }} | User: {{ userRef.name }}</p>
<p>Reactive Destructured: {{ name }} ({{ role }})</p>
<button @click="updateRef">Update Ref</button>
<button @click="updateReactive">Update Reactive</button>
</div>
</template>
reactive(): Calling reactive(0) or reactive('hello') does not work because JavaScript Proxies only wrap objects. Use ref() for numbers, strings, and booleans.reactive() Variable: Writing userState = reactive({ name: 'New' }) disconnects the template from the new proxy. Either mutate userState.name or switch to ref().reactive() Without toRefs(): Writing const { name } = userState extracts a plain string snapshot that stops updating when userState.name changes.ref() or reactive() in new Vue 3 code?Vue's official documentation recommends defaulting to ref(). It works for every data type, allows replacing whole objects cleanly, and makes reactive variables obvious thanks to .value.
ref values inside reactive()?Yes. If you place const count = ref(0) inside const state = reactive({ count }), reading state.count in script returns 0 without needing .value.
shallowRef() better than ref()?Use shallowRef() for large immutable datasets or third-party class instances where you only replace .value as a whole and do not need deep property tracking.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With