Vue Reactivity — ref, reactive, computed, watch
Vue's Reactivity System
Vue's reactivity system automatically tracks data dependencies and updates the DOM when data changes. In Vue 3's Composition API, you declare reactive state using ref() and reactive().
| API | Use for | Access value |
|---|---|---|
ref() | Primitives (string, number, boolean) and any value | count.value in JS, {{ count }} in template |
reactive() | Objects and arrays | state.name directly (no .value) |
computed() | Derived values — auto-updates when deps change | Like ref — .value in JS |
watch() | Side effects when data changes | Callback receives new/old value |
watchEffect() | Auto-track dependencies, run immediately | No explicit deps needed |
<template>
<div>
<!-- ref values -->
<p>Count: {{ count }}</p>
<p>Name: {{ name }}</p>
<button @click="count++">+1</button>
<!-- reactive object -->
<p>{{ user.name }} ({{ user.age }})</p>
<button @click="user.age++">Birthday</button>
<!-- computed -->
<p>Full name: {{ fullName }}</p>
<p>Double: {{ double }}</p>
<p>Filtered: {{ filteredItems.length }} items</p>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
// ref — for primitives
const count = ref(0)
const name = ref('Alice')
// reactive — for objects
const user = reactive({
firstName: 'Alice',
lastName: 'Smith',
age: 25
})
const items = reactive([
{ id: 1, name: 'Apple', active: true },
{ id: 2, name: 'Banana', active: false },
{ id: 3, name: 'Cherry', active: true },
])
// computed — derived, cached, auto-updates
const fullName = computed(() => `${user.firstName} ${user.lastName}`)
const double = computed(() => count.value * 2)
const filteredItems = computed(() => items.filter(i => i.active))
// Writable computed
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const parts = val.split(' ')
firstName.value = parts[0]
lastName.value = parts[1] || ''
}
})
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search..." />
<p>Results: {{ results.length }}</p>
<p>User: {{ user.name }}</p>
</div>
</template>
<script setup>
import { ref, reactive, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const results = ref([])
const user = reactive({ name: 'Alice', age: 25 })
// watch — explicit source, runs when it changes
watch(searchQuery, async (newVal, oldVal) => {
console.log(`Changed from "${oldVal}" to "${newVal}"`)
if (newVal.length > 2) {
results.value = await fetchResults(newVal)
}
})
// watch with options
watch(searchQuery, (newVal) => {
console.log('Debounced search:', newVal)
}, {
immediate: true, // run immediately on mount
deep: false // don't watch nested properties
})
// Watch reactive object — need deep: true for nested changes
watch(user, (newUser) => {
console.log('User changed:', newUser)
}, { deep: true })
// Watch specific property of reactive object
watch(() => user.age, (newAge) => {
console.log('Age changed to:', newAge)
})
// watchEffect — auto-tracks dependencies, runs immediately
watchEffect(() => {
// Automatically tracks searchQuery.value
console.log('Query is now:', searchQuery.value)
document.title = `Search: ${searchQuery.value}`
})
// Stop a watcher
const stop = watchEffect(() => { /* ... */ })
// stop() // call to stop watching
async function fetchResults(query) {
const res = await fetch(`/api/search?q=${query}`)
return res.json()
}
</script>
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.