In Vue 3, what do the watcher flush options 'pre', 'post' and 'sync' mean, and which one is the default?
answer
- relative to the owner's DOM update
- default reads pre-update DOM
- post and sync have aliases
- sync skips batching
basics
~20 s'pre' (the default) runs a watcher after parent updates but before its owner component's DOM update; 'post' runs it after Vue has updated the DOM; 'sync' runs it immediately on every change, with no batching. watchPostEffect and watchSyncEffect are the aliases.
solid answer
~40 s`flush` decides when a triggered watcher runs relative to Vue's rendering. The default, `'pre'`, queues the watcher and runs it after any parent component updates and before the owner component's own DOM update - so the callback still sees the pre-update DOM. `'post'` queues it after the DOM has been patched, so it can read the updated DOM and template refs; `watchPostEffect()` is the `watchEffect` alias. `'sync'` skips the queue: the watcher runs synchronously on every triggering mutation, with no batching, and `watchSyncEffect()` is its alias. The Vue guide warns to use `'sync'` with caution - fine for a simple boolean, wrong for something mutated many times, such as an array. The same option applies to `watch()` and `watchEffect()`.
code
ts · 17 linesimport { ref, watch, watchEffect, watchPostEffect, watchSyncEffect } from 'vue'
const page = ref(1)
// default 'pre': fix state before the owner re-renders
watch(page, (p) => { if (p < 1) page.value = 1 })
// 'post': read the DOM after Vue patched it
watchPostEffect(() => {
document.title = `Page ${page.value}`
})
// same as above, spelled out
watchEffect(() => { /* ... */ }, { flush: 'post' })
// 'sync': runs inside every mutation, no batching
watchSyncEffect(() => { console.log('page is', page.value) })go deeper
Know the default is 'pre', that 'post' is for reading the updated DOM, and that watchPostEffect is the shortcut.
Explain where each flush sits relative to the owner component's update, why the default suits state adjustments, and why 'sync' drops batching.
Diagnose DOM reads that see stale values and pick 'post' instead of scattering nextTick calls; reject 'sync' on collections in review.
Encourage a rule that DOM-touching watchers declare 'post' explicitly, so timing assumptions are visible rather than implicit.
## Why there is a `flush` option When you mutate reactive state, two kinds of work can be triggered: **component re-renders** and **your watchers**. Vue batches both in a queue that is flushed asynchronously, so a thousand synchronous writes produce one update. `flush` controls where a watcher sits relative to the component updates in that flush. It is accepted by `watch()` and `watchEffect()`, and it defaults to `'pre'`. ## The three values | `flush` | When the watcher runs | Batched? | DOM it sees | Alias | |---|---|---|---|---| | `'pre'` (default) | after parent component updates, before the owner component's DOM update | yes | owner's pre-update DOM | - | | `'post'` | after Vue has patched the DOM | yes | updated DOM | `watchPostEffect()` | | `'sync'` | immediately, on each triggering mutation | no | whatever is there right now | `watchSyncEffect()` | ## `'pre'` - the default - The watcher is queued and deduplicated: several changes in one tick produce one run. - It runs **before** the owner component re-renders. That is deliberate: a watcher that adjusts state (say, clamping a page number) can do so before rendering, so the component renders once with consistent state. - The flip side: if the callback reads the owner component's DOM, it sees the **old** DOM. - For `watchEffect`, the first run happens synchronously when the watcher is created; later runs are queued. ## `'post'` - after the DOM update - The watcher is queued after rendering, together with other post-render work. Template refs are assigned before post-flush watchers run, so they are available. - Use it when the side effect **reads or manipulates the DOM** the component just rendered: measuring an element, drawing on a canvas, handing a node to a third-party library. - For `watchEffect` with `flush: 'post'` (or `watchPostEffect()`), the **first** run is also deferred until after the component's DOM exists, instead of running synchronously in setup. - It is the declarative alternative to calling `nextTick()` inside a default watcher. ## `'sync'` - no queue at all - The watcher runs **synchronously inside the mutation**, before the assignment statement's next line executes. - No batching: `items.push()` called 1,000 times in a loop runs a sync watcher on `items.length` about 1,000 times, where a `'pre'` watcher would run once. - The Vue guide's advice: acceptable for watching a simple boolean, avoid it for data that may be mutated many times synchronously, such as arrays. - Legitimate uses are rare: keeping two pieces of state in lock-step where even a microtask of inconsistency is unacceptable, or instrumentation. ## The order in one flush 1. You mutate state. Only `'sync'` watchers run at this point; everything else is queued. 2. In a microtask, Vue flushes the queue in component order, parents before children. 3. Just before a component re-renders, its `'pre'` watchers that were triggered run, so state they adjust is included in that render. 4. The component re-renders and its DOM is patched. 5. After the patches, post-render work runs: template refs are assigned first, then `'post'` watchers. A watcher with the default flush that also changes state therefore costs no extra render, while a `'post'` watcher that changes state schedules another pass. ## Choosing 1. Start with the default. Most watchers change state or call APIs and do not care about the DOM. 2. Switch to `'post'` the moment the watcher touches the owner component's DOM. 3. Reach for `'sync'` only with a concrete reason, and never on collections. ## Common misunderstandings - "`'pre'` means before *any* DOM update." Parent components may already have updated; the guarantee is about the **owner** component. - "`'post'` runs after the browser paints." It runs after Vue patches the DOM, in the same flush; painting is up to the browser. - "`'sync'` is faster." It is more frequent, not faster; it removes batching. - "`flush` is only for `watchEffect`." `watch(source, cb, { flush: 'post' })` works the same way.
- In Vue 3, why is 'pre' a better default than 'post' for most watchers?Most watchers adjust state or start requests. Running them before the owner component re-renders lets a state adjustment land in the same render, so the component renders once with consistent data. A 'post' watcher that changed state would cause a second render right after the first. Only DOM-reading work needs to wait until after the patch.
- Does a Vue 3 watchPostEffect() run during server-side rendering?No. During SSR there is no DOM to wait for, and Vue does not set up watchers that would not run immediately; a post-flush effect gets a no-op handle on the server. DOM-touching code placed in a post-flush watcher is therefore naturally skipped on the server and runs on the client after hydration.
Think of the update queue as a mail run for one building. A 'pre' watcher gets its letter just before the owner's flat is redecorated, a 'post' watcher just after, and a 'sync' watcher is phoned the moment anything it depends on changes, outside the mail run entirely.
saying these in an interview costs you the question
- The default flush is 'post', so watchers always see the updated DOM.
- 'pre' means the watcher runs before any component has updated.
- flush: 'sync' is a performance optimisation for hot state.
- flush only applies to watchEffect(), not to watch().
- A 'post' watcher runs after the browser has painted the frame.