How to use watch() in Vue 3
watch() in Vue 3 runs a side effect in response to reactive state changes — fetching data when a filter changes, saving state to localStorage, or triggering animations when a value updates.
As the creator of CoreUI with Vue development experience since 2014, I use watch() whenever I need to react to a change with a side effect, and computed() when I just need a derived value.
The key difference from watchEffect() is that watch() is explicit — you declare exactly what to watch and receive the old and new values in the callback.
This makes it easier to understand what triggers the watcher and to compare values before and after.
Watch a single ref for changes.
import { ref, watch } from 'vue'
const userId = ref(1)
watch(userId, (newId, oldId) => {
console.log(`User changed from ${oldId} to ${newId}`)
fetchUser(newId)
})
// Trigger the watcher
userId.value = 2 // logs: "User changed from 1 to 2"
The watcher callback receives (newValue, oldValue). It only runs when the watched value changes — not on initial render. Use { immediate: true } to run it once when the component mounts and then again on subsequent changes.
Immediate Watch
Run the watcher immediately on mount.
import { ref, watch } from 'vue'
const searchQuery = ref('')
const results = ref([])
watch(
searchQuery,
async (query) => {
if (!query.trim()) {
results.value = []
return
}
results.value = await searchApi(query)
},
{ immediate: true }
)
{ immediate: true } fires the callback once with the current value before any changes. This is useful when the initial render needs to load data based on the current state without duplicating the fetch logic in onMounted.
Watching Multiple Sources
Watch several reactive values simultaneously.
import { ref, watch } from 'vue'
const page = ref(1)
const filters = ref({ category: 'all', sort: 'newest' })
watch(
[page, filters],
([newPage, newFilters], [oldPage, oldFilters]) => {
fetchData(newPage, newFilters)
},
{ deep: true }
)
Pass an array of sources to watch multiple values. The callback receives arrays of [newValues] and [oldValues] in matching order. { deep: true } makes Vue watch nested properties of filters — without it, replacing a nested property wouldn’t trigger the watcher.
Deep Watching Objects
Track changes to nested object properties.
import { ref, watch } from 'vue'
const settings = ref({
theme: 'light',
language: 'en',
notifications: {
email: true,
push: false
}
})
watch(
settings,
(newSettings) => {
localStorage.setItem('settings', JSON.stringify(newSettings))
},
{ deep: true }
)
// This triggers the watcher despite being nested
settings.value.notifications.push = true
Without { deep: true }, only replacing the entire settings object would trigger the watcher. With it, any change at any nesting depth triggers the callback. Note that with deep watching, newValue and oldValue reference the same object — use JSON stringify/parse if you need to compare.
Stopping a Watcher
Stop watching when it’s no longer needed.
const stop = watch(userId, fetchUser)
// Later, stop the watcher manually
stop()
Watchers created inside setup() are automatically stopped when the component unmounts. Manual stop is only needed when you create a watcher outside the component lifecycle (e.g., in a global store).
Best Practice Note
In CoreUI Vue components we use watch() for data fetching triggered by prop or state changes, and watchEffect() for more automatic dependency tracking. Prefer watch() when you need the old value for comparison or when you want explicit control over what triggers the side effect. See how to use watchEffect() in Vue 3 for cases where automatic dependency tracking is more convenient.



