reactive() and ref()
Vue 3's Composition API exposes two primary functions for creating reactive state: ref() and reactive(). Both are built on the same underlying Proxy-based reactivity system, but they differ in what kind of values they wrap and how you interact with them in JavaScript code. Understanding the distinction — and the tradeoffs of each — is essential to writing correct, idiomatic Composition API code.
Cricket analogy: Vue's ref() and reactive() are like two ways a scorer can track the game — a single running total (ref, wraps any value with .value) versus a full scoreboard object (reactive, wraps objects only) — both powered by the same tracking system but suited to different situations.
ref(): Reactivity for Any Value
ref() wraps a value of any type — primitive (string, number, boolean) or object — in a special reactive object with a single .value property. Reading or writing .value is what triggers dependency tracking and reactive updates. Vue's compiler and template system automatically "unwrap" refs, so inside a <template> you write {{ count }} rather than {{ count.value }}. Because ref() works uniformly for primitives and objects, and because reassigning .value to an entirely new object still works reactively, ref() is the recommended default for most standalone reactive state in the Composition API.
Cricket analogy: A single "current score" ref works whether it's holding a plain number or an entire innings-summary object — you read and write it via .value in code, but the scoreboard display auto-unwraps it so fans just see the number, and swapping in a whole new innings object still updates the display live.
reactive(): Deep Reactivity for Objects
reactive() takes an object (or array, Map, Set) and returns a Proxy of that object where property access and mutation are automatically tracked, without needing a .value wrapper — you just read and write properties directly, e.g. state.count++. However, reactive() has two notable constraints: it only works with object types (calling reactive() on a primitive like a number does nothing useful), and the reactivity is tied to the Proxy identity, meaning if you destructure properties out of a reactive object into standalone variables, or reassign the entire object, you lose the reactive connection. This is why Vue provides toRefs() to safely destructure a reactive object's properties into individual refs that retain reactivity.
Cricket analogy: A reactive() scoreboard object tracks runs and wickets directly (scoreboard.runs++) without a .value wrapper, but it only works because it's an object — and if a commentator pulls "runs" out into its own variable, that copy stops updating live; toRefs() is how you safely hand out individual live-updating stats to each commentator.
<script setup>
import { ref, reactive, toRefs } from 'vue'
// ref: works for primitives and objects alike
const count = ref(0)
// reactive: for a cohesive object of related state
const form = reactive({
username: '',
email: '',
agreedToTerms: false,
})
// Destructuring reactive() directly would break reactivity;
// toRefs() preserves it by returning individual refs
const { username, email } = toRefs(form)
function incrementCount() {
count.value++
}
function submitForm() {
form.agreedToTerms = true
}
</script>ref() and reactive() are interoperable: assigning an object to a ref (ref({ ... })) internally calls reactive() on that object if it's a plain object, and reactive() objects can contain nested refs, which are automatically unwrapped when accessed as properties (but not when accessed as array elements). This nested unwrapping behavior is a frequent source of subtle bugs, so it's worth testing carefully.
Directly destructuring a reactive() object — const { count } = reactive({ count: 0 }) — breaks reactivity because count becomes a plain, disconnected number. Always use toRefs() (or toRef() for a single property) when you need to pull properties out of a reactive object while preserving their reactive connection.
- ref() wraps any value type and requires .value access in script code (auto-unwrapped in templates).
- reactive() wraps objects/arrays/Maps/Sets in a Proxy with direct property access, no .value needed.
- reactive() only works on object types; calling it on a primitive has no reactive effect.
- Destructuring a reactive() object loses reactivity; use toRefs() to preserve it.
- ref() is generally the safer, more flexible default for standalone reactive state.
- Nested refs inside a reactive() object are auto-unwrapped when accessed as object properties.
Practice what you learned
1. What property must you access to read or write a ref's value in <script setup> JavaScript code?
2. Why does calling reactive() on a primitive number not provide useful reactivity?
3. What happens if you destructure a property directly out of a reactive() object into a standalone variable?
4. Which utility function converts a reactive object's properties into individual refs while preserving reactivity?
5. In a Vue template, how do you access a ref named `count` defined in <script setup>?
Was this page helpful?
You May Also Like
Computed Properties
How Vue's computed() function derives cached, reactive values from other state, and why it's preferred over methods for derived data in templates.
Watchers Explained
How watch() and watchEffect() let you run side effects in response to reactive state changes, and how to choose between them.
Composables Explained
Understand what composables are, why they replace mixins as Vue's primary reuse pattern, and the conventions they follow.
Local Component State Patterns
Explore idiomatic ways to structure a component's own internal reactive state before reaching for external state management like Pinia.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics