skip to content

In Vue 3, what do the watcher flush options 'pre', 'post' and 'sync' mean, and which one is the default?

level: middleimportance: should knowfreq 50%

answer

  1. relative to the owner's DOM update
  2. default reads pre-update DOM
  3. post and sync have aliases
  4. 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 lines
ts
import { 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

for a junior

Know the default is 'pre', that 'post' is for reading the updated DOM, and that watchPostEffect is the shortcut.

for a middle

Explain where each flush sits relative to the owner component's update, why the default suits state adjustments, and why 'sync' drops batching.

for a senior

Diagnose DOM reads that see stale values and pick 'post' instead of scattering nextTick calls; reject 'sync' on collections in review.

for a principal

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.