If your reactive side effects aren't properly timed in a Vue app, you'll see very confusing behavior.
To fully understand Vue effect timing, you'd have to learn about microtasks in browser-based JavaScript. That said, a deep understanding of microtasks and of the browser event loop is really only practical for framework authors—it's not practical knowledge for framework users.
Instead of trying to learn how Vue effect timing actually works under the hood, try learning the following:
- Simplified versions of effect timing concepts
- Some opinionated guidelines on how to use the two effect timing tools at your disposal in Vue 3: the
flush
option forwatch
andwatchEffect
, andnextTick
.
flush: 'sync'
runs effects right awayflush: 'pre'
runs effects asynchronously, always before the DOM is updatedflush: 'post'
runs effects asynchronously, always after the DOM is updated, but always before the browser actually lays out and repaints your web app based on your DOM changesnextTick
runs effects asynchronously, always after the DOM is updated, but not always before the browser actually lays out and repaints your web app based on your DOM changes
To see how effects run exactly in that order, check out this demo on the SFC playground.
#1: Use flush: 'pre'
if your side effect does not access or change a DOM element that has reactive bindings, or a DOM element rendered by v-for
. (This is the default watch
and watchEffect
setting.)
// Example code for a reactive tablist component
const selectedTabIndex = ref(0),
selectedTabPanelIndex = ref(0)
// No DOM changes or DOM access here, so flush 'pre' is fine
if (props.selectsPanelWhenTabIsFocused) {
watch(
selectedTabIndex,
() => selectedTabPanelIndex.value = selectedTabIndex.value,
)
}
#2: Use flush: 'post'
if your side effect does access or change a DOM element that has reactive bindings, or a DOM element rendered by v-for
.
// Example code for a selectable HTML input
const selection = ref({ start: 0, end: 0, direction: 'none' }),
element = ref(null)
// This effect code *does* update a DOM element, so flush 'post' is needed
watch(
selection,
() => {
element.value.setSelectionRange(
selection.start,
selection.end,
selection.direction,
)
},
{ flush: 'post' }
)
#3: Avoid nextTick
in production code. Only use it when you're writing a test and you need to ensure that Vue's component update cycle is complete before you retrieve your expected state from the DOM.
// Example code, testing a tablist component
const secondTabElement = document.querySelectorAll('button')[1]
await page.click(secondTabElement)
// Use nextTick to ensure that all effects have been flushed,
// and that the browser has had time to react to changes.
await window.nextTick()
// Now, safely access expected values.
const selectedTabPanelElement = document.querySelector('[aria-hidden="false"]')
expect(selectedTabPanelElement.textContent).toEqual('Tab #2')
I'm sure there's a use case for flush: 'sync'
, but I haven't found it yet!
- A lot more real-world code where I use effect timing extensively
- Effect flush timing docs
- Async update queue docs
- In depth: Microtasks and the JavaScript runtime environment
- These four podcast episodes (~10 mins each)
- Event loop: microtasks and macrotasks