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>
Understanding Reactivity is not just about syntax. In production applications, this topic directly affects maintainability, debugging speed, and team collaboration. Focus on readability, small reusable patterns, and predictable state flow when implementing Reactivity.
A practical approach is to first implement the simplest working version, then refactor into reusable pieces (components/composables/stores) only when duplication appears. This helps keep your Vue codebase clean while avoiding over-engineering.
Explore 500+ free tutorials across 20+ languages and frameworks.