Skip to content

Instantly share code, notes, and snippets.

@AlexVipond
Last active September 5, 2023 09:42
Show Gist options
  • Save AlexVipond/521c2452a2b7c94d950389638f22be58 to your computer and use it in GitHub Desktop.
Save AlexVipond/521c2452a2b7c94d950389638f22be58 to your computer and use it in GitHub Desktop.
Effect timing in Vue 3

Effect timing in Vue 3

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 for watch and watchEffect, and nextTick.

Effect timing concepts

  • flush: 'sync' runs effects right away
  • flush: 'pre' runs effects asynchronously, always before the DOM is updated
  • flush: '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 changes
  • nextTick 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.

Using effect timing in practice

#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')

#4: Avoid flush: 'sync'.

I'm sure there's a use case for flush: 'sync', but I haven't found it yet!

#5: If you see unexpected behavior, break one of the previous rules to troubleshoot.

Further reading/listening

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment