In Vue 3, how do watch()'s deep, immediate and once options change when and how often the callback runs?
answer
- lazy and shallow by default
- traversal, capped by a number in 3.5
- eager first call, undefined old value
- once stops after one callback (3.4)
basics
~20 sdeep makes a ref or getter source fire on nested mutations, and since 3.5 a number caps traversal depth; immediate runs the callback at creation with an undefined old value; once (3.4+) stops the watcher after its first callback.
solid answer
~40 s`watch()` is lazy, and shallow for ref and getter sources. `deep: true` makes Vue traverse the source's nested properties so a mutation anywhere fires the callback; a reactive-object source is already deep, and `deep: false` on it limits tracking to root-level properties. Since Vue 3.5, `deep` also accepts a number, the maximum traversal depth, which bounds the cost on large structures. `immediate: true` runs the callback once at creation with `undefined` as the old value, which suits load-then-reload logic. `once: true` (3.4+) stops the watcher after its first callback; combined with `immediate`, that first callback is the eager one, so it never reacts to a change. `flush` is a separate timing option.
code
ts · 16 linesimport { reactive, ref, watch } from 'vue'
const id = ref(1)
const state = reactive({ filters: { category: 'all', range: { min: 0, max: 100 } } })
// immediate: load now, reload on every id change
watch(id, (next, prev) => console.log('load', next, 'was', prev), { immediate: true })
// logs: load 1 was undefined
// numeric deep (3.5+): root-level fields of filters only
watch(() => state.filters, () => console.log('filters changed'), { deep: 1 })
state.filters.category = 'books' // fires
state.filters.range.min = 5 // does not fire
// once (3.4+): react to the first change, then stop
watch(id, (next) => console.log('first change to', next), { once: true })go deeper
Know the three options: deep for nested changes, immediate to run at creation, once to run a single time. watch() is lazy without immediate.
Explain deep as traversal that tracks nested reads, the numeric depth cap in 3.5, the undefined first old value, and how once interacts with immediate.
Judge cost: deep traversal on big structures runs every trigger; prefer narrow getters or numeric depth, and use immediate instead of duplicated setup calls.
Set conventions for deep watching in shared code: when a deep watcher needs a written justification, and when data shape should change instead.
## The defaults Vue 3's `watch(source, callback, options)` is **lazy** and, for ref and getter sources, **shallow** by default: - lazy: the callback does not run when the watcher is created, only after the source changes; - shallow: a ref or getter source fires when its value is replaced or its returned value changes, not when something nested inside that value is mutated; - the exception is a **reactive object** passed directly, which is watched deeply without any option. The API reference types list `immediate?: boolean` (default `false`), `deep?: boolean | number` (default `false`) and `once?: boolean` (default `false`, 3.4+). `flush` is a fourth option that controls *when* in the update cycle the callback runs; it is a separate timing topic. ## deep `deep: true` makes Vue **traverse** the source's value on each run, reading every nested property so that each one becomes a tracked dependency. Two effects follow: 1. any nested mutation re-runs the watcher; 2. in deep mode the callback fires on every such trigger without comparing old and new values, which is also why the two are often the same object. | Source | no `deep` | `deep: true` | `deep: false` | `deep: N` (3.5+) | |---|---|---|---|---| | ref | `.value` replaced | any nested change | as default | nested up to N levels | | getter | return value changes | any nested change | as default | nested up to N levels | | reactive object | any nested change (implicit) | any nested change | root-level properties only | nested up to N levels | Traversal walks plain objects, arrays, `Map`s and `Set`s, and skips values marked with `markRaw()`. It runs on every trigger, so on a large structure the tracking itself becomes the cost. ## Numeric depth (Vue 3.5+) Since 3.5, `deep` also accepts a number: the **maximum traversal depth**. `deep: 1` tracks the root-level properties of the value the source returns; `deep: 2` also tracks the properties of objects one level down, and so on. For `watch(() => state.filters, cb, { deep: 1 })`, assigning `state.filters.category` fires the callback, but `state.filters.range.min = 5` does not, because `range` is read as a value and not entered. Numeric depth is the tool for a big nested structure where only the top layers matter. ## immediate `immediate: true` runs the callback **once at creation**, during the `watch()` call itself, and then continues lazily. On that first call the old value is `undefined` for a single source; for an array source the old-values argument is an empty array. The canonical use is load-then-reload: fetch data for the current id at setup and again whenever it changes, with one callback instead of a duplicated initial call. ## once (Vue 3.4+) `once: true` wraps the callback so that the watcher **stops itself after the callback's first run**. It suits one-shot reactions such as 'when the first result arrives, focus the list'. The combination with `immediate` is a known edge: | Options | Callback runs | |---|---| | none | on every change | | `immediate` | at creation, then on every change | | `once` | on the first change only | | `immediate` + `once` | at creation only; never for a later change | ## Where the options do nothing `deep`, `immediate` and `once` belong to the `watch(source, callback, options)` signature. Passing them to `watchEffect()` has no effect, and in development Vue warns that the option is only respected when using that signature. ## Picking options in practice - Reach for a narrower getter before reaching for `deep: true`. - Use a number for `deep` when the structure is large and the relevant changes are shallow. - Use `immediate` instead of duplicating the callback body at the top of `setup`. - Use `once` for genuinely one-time reactions, and remember what `immediate` does to it.
- What does `watch(src, cb, { immediate: true, once: true })` do?It runs the callback once at creation and then stops the watcher, so it never reacts to a later change. `once` counts the eager call as the first callback. If you want 'run now and on the first change', drop `once` and stop the watcher yourself after the second call, or use two watchers.
- What does `deep: false` do on a reactive object source?It limits tracking to the object's root-level properties. Assigning `state.category` still fires the callback, but mutating `state.range.min` does not, because traversal does not enter nested objects. Without the option, a reactive object source is traversed fully.
saying these in an interview costs you the question
- immediate: true passes the current value as oldValue on the first call
- deep: true is needed to see nested changes on a reactive object source
- once: true means the callback runs once per component render
- A numeric deep value is a debounce delay in milliseconds
- deep and immediate work the same way when passed to watchEffect()