Vue.js State Management: When to Use refs Over reactive() in Composition API
Choose refs for primitive values and simple state, reactive() for complex objects. Refs auto-unwrap in templates but require .value in JS code.
07 Dec 2025, 23:28 UTC

The Decision Point: Choosing Between refs and reactive()
When building Vue 3 applications with the Composition API, you'll frequently need to decide between refs and reactive() for state management. This decision impacts code readability, TypeScript support, and how you interact with your data throughout your application.
The Core Trade-off
The fundamental choice comes down to how you want to access and modify your state. refs provide a consistent API that works for both primitive values and objects, while reactive() creates a reactive proxy object that feels more like working with plain JavaScript objects.
Quick Comparison Table
| Feature | refs | reactive() |
|---|---|---|
| Initial Syntax | ref(0) or ref({}) |
reactive({}) |
| Access in Template | Auto-unwrapped: {{ count }} |
Direct: {{ count }} |
| Access in JS | Manual: count.value |
Direct: count |
| TypeScript Support | Strong, with .value inference | Good, but requires careful typing |
| Primitive Values | Built-in support | Requires wrapper object |
| Shallow Reactivity | Yes (shallowRef) | No (use shallowReactive) |
Practical Implementation Example
Let's implement a counter component that demonstrates the key differences:
<script setup>
import { ref, reactive } from 'vue'
// Using ref for a primitive value
const count = ref(0)
// Using reactive for an object
const userState = reactive({
name: 'John',
age: 30,
active: true
})
// Method to increment count
function increment() {
count.value++ // Must use .value in JS
}
// Method to update user
function updateUser(newAge) {
userState.age = newAge // Direct access in JS
}
</script>
<template>
<div>
<h2>Using ref: {{ count }}</h2>
<h2>Using reactive: {{ userState.name }} is {{ userState.age }}</h2>
<button @click="increment">Increment Count</button>
<button @click="updateUser(31)">Update Age</button>
</div>
</template>
Key Behavioral Differences
Template Auto-unwrapping: In templates, refs are automatically unwrapped, so you can use {{ count }} instead of {{ count.value }}. This is a convenience that makes templates cleaner but can cause confusion when you intentionally need the ref object itself.
JS Code Access: In JavaScript code, you must always use .value to access or modify a ref's value. This explicitness helps you understand when you're working with the ref wrapper versus the actual value.
TypeScript Integration
Vue's Composition API provides excellent TypeScript support. When you declare const count = ref(0), TypeScript knows that count is a Ref<number>, and count.value is a number. In templates, Vue's compiler automatically unwraps refs, so you don't need to worry about type errors when accessing ref values directly.
When to Choose refs
- Primitive values: For numbers, strings, booleans, or null/undefined values
- Values that might become objects: If you're unsure whether your state will be a primitive or object later
- Simple state: When you want the simplest possible API for basic reactivity
- Template-focused access: When most of your interactions are in templates rather than complex JS logic
When to Choose reactive()
- Object state: For complex state objects with multiple properties
- Deeply nested objects: When you need to modify nested properties frequently
- Array operations: For arrays where you need to use array methods directly
- Proxy-like behavior: When you want state that behaves exactly like plain JavaScript objects
Validation and Debugging
To verify your reactivity is working correctly:
- Use Vue DevTools: Install the Vue DevTools browser extension and inspect your component's state. You should see refs and reactive objects with their current values updating as you interact with your component.
- Test template rendering: Create a simple test that verifies
{{ count }}displays the correct value without needing.value. - Check reactivity in methods: Ensure that modifying
count.valuein a method triggers the expected template updates.
Common Pitfalls to Avoid
Template vs JS confusion: Remember that in templates you access {{ count }} but in JavaScript you need count.value. Using the wrong syntax in the wrong context is a common source of bugs.
Deep nested refs: While you can create refs of refs, this adds unnecessary complexity. Use toRefs() when you need to destructure a reactive object while preserving reactivity.
Conclusion
For most day-to-day Vue 3 development, refs are the better default choice. They provide a consistent API, excellent TypeScript support, and work seamlessly in templates. Reserve reactive() for cases where you're working with complex object state that benefits from proxy-like behavior.
The key is understanding that refs require explicit .value access in JavaScript but provide automatic unwrapping in templates, making them ideal for a wide range of use cases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.