The Difference Between a Post Flush Watcher and nextTick in Vue

If you need your watcher to run after the DOM has updated, you turn to a watcher with flush: 'post':

const items = ref([])
function addItem(newItem) {
items.value.push(newItem)
}
watch(items, () => {
// The DOM has been updated now
// Do something with the updated list of items
}, { flush: 'post' })

But we also have the nextTick function. The docs state that nextTick is “A utility for waiting for the next DOM update flush.” So it appears that it does exactly the same thing.

How are these two approaches different, and does it matter for your code?

The short answer: it’s a single microtask, and it doesn’t matter.

The long answer continues below.

Comparing the two approaches

These two implementations will both work, and they’ll both give you the same result (most of the time):

const items = ref([])
function addItem(newItem) {
items.value.push(newItem)
}
// 1. Using a post flush watcher
watch(items, () => {
// The DOM has been updated now
// Do something with the updated list of items
}, { flush: 'post' })
// 2. Using a pre flush watcher with nextTick
watch(items, async () => {
// The watcher gets called before the DOM updates
// But we can wait by using nextTick
await nextTick()
// The DOM has been updated now
// Do something with the updated list of items
})

These two approaches are basically equivalent. By sticking the nextTick inside of the watcher, it becomes even more clear how similar the code ends up becoming.

If we’re only mutating the items ref inside of the addItem function, we don’t need a watcher at all and we could further simplify it by just using nextTick there:

const items = ref([])
async function addItem(newItem) {
items.value.push(newItem)
await nextTick()
// The DOM has been updated now
// Do something with the updated list of items
}

Although nearly identical, there is a slight difference based on how Vue treats post flush watchers and nextTick effects.

Digging into how nextTick works

Let’s take a look at the implementation of nextTick itself:

export function nextTick<T = void, R = void>(
this: T,
fn?: (this: T) => R,
): Promise<Awaited<R>> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}

We can remove some TypeScript noise to simplify it:

export function nextTick(this, fn) {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}

The this value is bound to the component instance when it’s used with the Options API. Otherwise, it’s not bound because we don’t use this in the Compositions API like we used to.

We can use nextTick in two different ways:

// With await
await nextTick()
// Passing in a function
nextTick(() => {})

To make the implementation clearer, I’ll rewrite nextTick in both of these ways, and remove the support for the Options API as well:

// With await
export function nextTick() {
return currentFlushPromise || resolvedPromise
}
// Passing in a function
export function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise
return p.then(fn)
}

Now we can see that nextTick is basically just returning the currentFlushPromise. If you pass in a function though, it has to do some extra work to run that function once the currentFlushPromise resolves.

So, in our components, we can think of nextTick as being this:

await currentFlushPromise

Of course, now we need to figure out what currentFlushPromise does, and you’d be right in thinking it’s somehow related to the flush property of our watchers.

Flushing jobs with microtasks

In the source code, our currentFlushPromise is defined like this:

currentFlushPromise = Promise.resolve().then(flushJobs)

We use Promise.resolve so that we can run flushJobs in the next microtask. If you’re unfamiliar with microtasks, this means that instead of executing flushJobs immediately, we’ll wait until we’ve reached the very end of our execution, and then run flushJobs:

console.log('1')
// Wait until we're done with everything else
Promise.resolve().then(() => console.log('3')
console.log('2')

I highly recommend reading Jake Archibald’s fantastic post on tasks in Javascript to understand them better. It’s very much worth it if you’re a Javascript developer (which you are if you’re reading this).

This flushJobs function will call of the jobs that have been queued, flushing them. And at the very end, we flush all of the post flush jobs as well.

To quickly recap what we’ve covered so far:

  • await nextTick() is the same as await currentFlushPromise
  • currentFlushPromise queues up the flushJobs method in the next microtask
  • flushJobs does the work of calling of our queued up post flush jobs

Next, we need to figure out how post flush watchers are connected to all of this.

Figuring out where watchers fit in all of this

Somehow, it seems that a post flush watcher ends up as one of these post flush callbacks. They are certainly named very similarly. We just need to trace through the source code to make sure that this is true.

Looking at the source code, when we run a watcher with flush: "post" it gets set up to queue the callback we pass in as a “post render effect” using queuePostRenderEffect. If you dig around in the source code enough, you’ll discover that queuePostRenderEffect is effectively just calling the function queuePostFlushCb but with some extra stuff to handle Suspense.

So, ultimately, setting up a post flush watcher uses queuePostFlushCb, which does exactly what it sounds like it does — queues up our post flush watcher as a post flush job.

And that’s the final piece of this puzzle!

This is the flow:

  • A post flush watcher gets queued up as a post flush job
  • currentFlushPromise queues up the flushJobs method in the next microtask
  • flushJobs does the work of calling of our queued up post flush jobs
  • await nextTick() is the same as await currentFlushPromise

From this, we can see that our post flush watcher is executed inside of flushJobs, which is inside of currentFlushPromise. So, to rewrite nextTick in a pseudocode:

async function nextTick() {
await runPostFlushWatchers()
}

When we write await nextTick(), we’re also awaiting on all of the post flush watchers.

The difference is just a microtask

When we use a post flush watcher, it gets flushed at the end of the flushJobs method. When we use nextTick, however, it gets run in the very next microtask, right after flushJobs.

This is because using nextTick is the same as await currentFlushPromise, and we know that currentFlushPromise involves calling flushJobs:

currentFlushPromise = Promise.resolve().then(flushJobs)

So we have this chain:

currentFlushPromise
-> flushJobs
-> post flush watchers
nextTick
-> our code after nextTick

The difference between these two implementations is only a single microtask. A post flush watcher will execute slightly earlier, but in practice this won’t make any real difference. Here’s our example from earlier:

const items = ref([])
function addItem(newItem) {
items.value.push(newItem)
}
// 1. Using a post flush watcher
watch(items, () => {
// The DOM has been updated now
// Do something with the updated list of items
}, { flush: 'post' })
// 2. Using a pre flush watcher with nextTick
watch(items, async () => {
// The watcher gets called before the DOM updates
// But we can wait by using nextTick
await nextTick()
// The DOM has been updated now
// Do something with the updated list of items
})

However, if you have some really nasty bug that you’re dealing with, it might be helpful to know that you can force one set of watchers to always run last using nextTick. In the above example, the second implementation will always run last, no matter what.

If you’re in this situation though, I wish you the best of luck. For the rest of us, these are functionally exactly the same.

The only meaningful difference is in code readability. Here, though, I don’t have any opinions (yet). In both cases, it’s clear that you’re waiting to do something until after the DOM updates. If you do have an opinion, I’m open to hearing them!

In the end, this was all just a long and complicated way of saying: it doesn’t really matter.

Advanced Reactivity

If you’re interested in learning more about advanced reactivity in Vue, check out my course Advanced Reactivity.

Instead of deep dives into the internals of Vue, the course has 22 exercises that will you master the advanced parts of Vue's reactivity system:

  • Effect Scopes (temporary and long running)
  • Watchers (post flush, pre flush, and how to make them bulletproof)
  • Custom refs, so you can have fine-grained control over how your refs work
  • Writable computed refs, which open up a whole new world of possibilities
  • and more!

You can check out all the details here: Advanced Reactivity.