Watchers
Run a side effect when state changes with watch() and watchEffect(), and know when a computed is the better tool.
What you'll learn
When you finish, you can do these.- 1Run code with watch() each time a value changes
- 2Explain why two quick changes run a watcher once
- 3Save a value on every change with watchEffect()
- 4Fix a watcher that works out a value or never runs
Picture a shop page with a search box over a list of products. Type mug, and the list narrows to Mug and Mug warmer, a change you see on the page. Other work shows nothing on the page, yet should still run each time the text changes: saving the search so it survives a reload, or fetching results from a server.
A watcher is the place for that work: a function Vue runs each time a value it watches changes. The Code tab holds that search page, the file App.vue, with Vue's two kinds of watcher: one logs each change of the text, the other saves it. You need one idea from the Computed properties lesson: a computed, a value Vue works out from other values and keeps up to date. By the end you can write both and tell when a computed fits better.
Side effects and watch()
watch(), the first kind, runs your code after a value you name changes. That code is usually a side effect: it reaches outside the component, to save, log or call a server. A computed must not do that, because its only job is to return a value.
In App.vue, v-model ties the search box to query, a ref (a box that holds a value and tells Vue when it changes), and results, a computed, lists the products that match it.
watch() takes a source, what to watch, and a callback, the function Vue calls after the source changes, with the new value and the old one. Here the source is query, and the callback writes the text and the time into lastSearched and adds a line to log, the list at the bottom of the page:
watch(query, (next, prev) => {
lastSearched.value = `"${next}" at ${new Date().toLocaleTimeString()}`
log.value.unshift({ id: nextId++, text: `"${prev}" → "${next}"` })
})
Type in the search box: the Last searched line shows the new text and the time, and each keystroke adds a line such as "mu" → "mug" to the log, newest at the top.
A source can take three forms.
- A ref, as in the search watcher:
watch(query, …). - A getter, a function that returns the value to watch:
watch(() => query.value.trim(), …). The callback runs only when the trimmed text changes, so a space typed at the end does nothing. - An array of sources, such as
queryand acategoryref:watch([query, category], ([q, c], [oldQ, oldC]) => …). The callback runs when either one changes and receives both values.
A watcher made in <script setup> stops by itself when the component leaves the page, so you need not stop it in onUnmounted(), the hook Vue runs at that moment (Lifecycle and template refs).
When the callback runs
Vue does not call the callback the moment you assign a value. It puts the watcher in a queue and runs it once, after your code finishes, with the value at that point.
state changesqueuedyour code finishes first; flush: sync runs at oncecallback runswith the new and old value, before the re-rendercomponent re-rendersflush: post runs the callback after this instead
So two changes in a row produce one callback. In App.vue, the button labelled Cap, then mug runs pickTwice, which makes two changes:
function pickTwice() {
query.value = 'cap'
query.value = 'mug'
}
Clear the search box and click the button. The log gets one line, "" → "mug": the callback never saw cap. Click it again and no line appears: by the time the watcher runs, the value is mug again, the same as before.
watch() is also lazy: it waits for the first change and does not run when the page loads. That is why Last searched says nothing yet until you type, even when the box holds a saved search. Pass { immediate: true } as a third argument to run it at the start too.
watchEffect() tracks what it reads
watchEffect(), the second kind, takes only a function. It runs it once straight away, records every ref it reads, and runs it again when any of them changes. You name no source: what the function reads is the source.
App.vue uses one to save the search text:
watchEffect(() => {
localStorage.setItem('last-search', query.value)
})
It reads query.value, so it runs on every change and saves the text in localStorage, the browser's own storage. The line that creates query reads it back on load, so your search survives a restart of the preview.
Pick watch() when you need the old value, want to skip the first run, or want to say exactly what triggers the work. Pick watchEffect() when the work should run at once and its inputs are plain to see.
What goes wrong
The most common mistake uses a watcher to work out a value the page shows. Say a line above the list counts the matches, <p>{{ matches }} matches</p>, and a watcher copies the length of results, the matching list, into a ref:
const matches = ref(0)
watch(results, (list) => {
matches.value = list.length
})
Until you type, the line says 0 matches while the list shows products, because watch() does not run at the start. The ref is also a second copy that can fall out of step with the list. A computed is right from the first render and keeps no copy.
const matches = computed(() => results.value.length)
The second mistake hands watch() a plain value. It is easy to make with reactive(), which keeps related fields in one object (Reactivity basics). Say App.vue also keeps a counter in such an object and saves it on every change:
const state = reactive({ count: 0 })
watch(state.count, (next) => {
localStorage.setItem('count', String(next))
})
state.count is the number 0 at the moment the line runs, and a number cannot change. Vue warns Invalid watch source: 0 A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types. and the callback never runs. Wrap the property in a getter, a function that returns it, so Vue reads it again each time.
const state = reactive({ count: 0 })
watch(() => state.count, (next) => {
localStorage.setItem('count', String(next))
})
What to remember
- A watcher runs a side effect, such as saving, logging or fetching, when state changes. A value the page shows belongs in a computed.
watch(source, (next, prev) => …)takes a ref, a getter or an array of them, and passes the new and the old value.- Vue queues the callback and runs it once after your code finishes, so two quick changes give one call with the final value.
watch()waits for the first change.watchEffect()runs at once and runs again when anything it read changes.- Watch a property of a reactive object through a getter,
() => state.count, neverstate.countitself.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Make
watchEffectshow what it reads. Inside it, addlog.value.unshift({ id: nextId++, text: `effect read "${query.value}"` }). A line appears as soon as the page loads, becausewatchEffectruns once straight away. Each keystroke then adds an effect line beside the watch line. - Watch two sources. Add
const category = ref('all')and a<select v-model="category">with the options all, kitchen and wear. Changewatch(query, …)towatch([query, category], ([q, c], [oldQ, oldC]) => …)and log`${oldQ}/${oldC} → ${q}/${c}`. Picking kitchen in the select now adds a line, and so does typing. - Make the first mistake from What goes wrong on purpose: its
matchesref and watcher, and<p>{{ matches }} matches</p>above the list. On load it says 0 matches while the list shows several products; type a letter and it catches up. Swap inconst matches = computed(() => results.value.length), and the number is right from the first render.