Skip to content
← All questions
🌱 Beginner

What's the difference between ref and reactive?

⚛️ Reactivity🔗 Composition API

Vue 3 gives you two ways to create reactive state: ref and reactive. They both make data reactive so the template updates when it changes, but they work differently and have different constraints.

ref: works with any value

ref wraps any value: a number, a string, a boolean, an object, an array, or null. You access and modify it through .value in JavaScript/TypeScript. In templates, Vue unwraps it automatically so you don't write .value there.

ts
import { ref } from 'vue'

const count = ref(0) // primitive
const user = ref({ name: '' }) // object
const items = ref<string[]>([]) // array

count.value++ // access through .value in JS
user.value.name = 'Ana' // nested access
items.value.push('new item')
vue
<template>
  <!-- No .value needed in templates -->
  <p>{{ count }}</p>
  <p>{{ user.name }}</p>
</template>
Open in Vue Playground

reactive: objects only, no .value

reactive wraps an object (or array, Map, Set) and makes its properties reactive directly, no .value needed. But it only works with objects. You can't pass it a number or string.

ts
import { reactive } from 'vue'

const state = reactive({ count: 0, name: '' })

state.count++ // no .value needed
state.name = 'Ana'

The big limitation: you cannot reassign a reactive object. If you do state = newObject, you break the reactive link. Watchers and the template still reference the old object.

ts
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // ❌ breaks reactivity — old watchers still watch the old proxy

When to use which

Use ref for everything is the safest default. It works with any type, you can reassign it freely (count.value = newValue), and it handles all edge cases. The .value is a small cost for flexibility.

Use reactive when you have a group of related properties that always live together and you'll never replace the whole object, like a form:

ts
const form = reactive({
  email: '',
  password: '',
  remember: false
})

form.email = 'ana@example.com' // ergonomic, no .value

The gotchas

Destructuring a reactive breaks reactivity:

ts
const state = reactive({ count: 0 })
const { count } = state // ❌ count is now a plain number (0), not reactive
// Use toRefs() if you need to destructure

Reassigning a ref is fine, reassigning a reactive is not:

ts
const data = ref<User[]>([])
data.value = await fetchUsers() // ✅ works perfectly

let data = reactive<User[]>([])
data = await fetchUsers() // ❌ breaks reactive link

This is why ref is the recommended default. When you need to replace the entire value (API responses, resetting state), ref just works.

See also: What's the difference between computed and watch? · Why do I lose reactivity when destructuring?

References

Released under the MIT License.