nextTick
Vue batches DOM updates, so the element is still stale on the next line; nextTick waits for the flush without a timeout.
await nextTick() // nextTick(callback?: () => void): Promise<void>Changing reactive state does not change the DOM on the next line. Vue buffers the update and applies it on the "next tick", after your current synchronous code finishes, so a component re-renders once however many values you changed. nextTick() is how you wait for that flush.
Two forms
It returns a promise that resolves once the pending DOM update has been applied, and it also accepts a callback. await nextTick() and nextTick(() => { ... }) are the same thing; the promise form usually reads better inside an async handler. Push a row, await, and the new element is in the document and can be measured or scrolled to.
Not a timeout
setTimeout(fn, 0) normally lands after the flush, which is why it looks like it works, but nothing guarantees that ordering. Code written that way fails on a slow machine and the usual next step is to raise the delay, which hides the problem instead of fixing it.
If a DOM-reading effect should run after every change to some state, rather than at one call site, a watcher with flush: 'post' is the better tool: the callback runs after Vue has patched the DOM. The default, flush: 'pre', runs it before.
What trips people up
nextTick only waits for changes made before the call. State you change afterwards needs another await. And it waits for a DOM flush, not for data: a value arriving from a request shows up when that promise resolves, which has nothing to do with ticks.
Example
<script setup lang="ts">
import { nextTick, ref, useTemplateRef } from 'vue'
interface Message {
id: number
text: string
}
const messages = ref<Message[]>([{ id: 1, text: 'Thanks for waiting.' }])
const draft = ref('')
const log = useTemplateRef<HTMLUListElement>('log')
let nextId = 2
async function send() {
const text = draft.value.trim()
if (text === '') return
messages.value.push({ id: nextId, text })
nextId += 1
draft.value = ''
await nextTick()
const el = log.value
if (el) {
el.scrollTop = el.scrollHeight
}
}
</script>
<template>
<div class="chat">
<ul ref="log" class="log">
<li v-for="message in messages" :key="message.id">{{ message.text }}</li>
</ul>
<form @submit.prevent="send">
<input v-model="draft" type="text" />
<button type="submit">Send</button>
</form>
</div>
</template>
<style scoped>
.log {
height: 8rem;
overflow-y: auto;
}
</style>When to use it
- Scroll a message list to the bottom after appending a message.
- Focus or select an input that a v-if has only just revealed.
- Measure an element's new size after its content changed.
- Check in a test that the rendered output reflects a state change you just made.
More examples
Callback form and promise form
The two functions do exactly the same thing: nextTick returns a promise and also takes a callback, and the callback runs at the moment the promise would resolve. The "before flush" line is the point of the example — it prints the previous number, because the paragraph has not been patched yet. Use the callback form when you are not in an async function and do not want to make one.
<script setup lang="ts">
import { nextTick, ref, useTemplateRef } from 'vue'
const count = ref(0)
const label = useTemplateRef<HTMLParagraphElement>('label')
function withCallback() {
count.value += 1
console.log('before flush', label.value?.textContent)
nextTick(() => {
console.log('after flush', label.value?.textContent)
})
}
async function withAwait() {
count.value += 1
await nextTick()
console.log('after flush', label.value?.textContent)
}
</script>
<template>
<div class="demo">
<p ref="label">{{ count }}</p>
<button type="button" @click="withCallback">Callback form</button>
<button type="button" @click="withAwait">Await form</button>
</div>
</template>
<style scoped>
.demo {
display: flex;
gap: 0.5rem;
align-items: center;
}
</style>A post-flush watcher instead of calling nextTick everywhere
When the rows change, the measurement has to happen after the DOM is patched. flush: 'post' does that for every change to the watched source, so no caller has to remember an await. Without the option the watcher runs with the default flush: 'pre', before the update, and scrollHeight would be the old value. Prefer this when the effect belongs to the state; prefer nextTick when it belongs to one particular action.
<script setup lang="ts">
import { ref, useTemplateRef, watch } from 'vue'
const props = defineProps<{ rows: string[] }>()
const box = useTemplateRef<HTMLDivElement>('box')
const height = ref(0)
watch(
() => props.rows.length,
() => {
height.value = box.value?.scrollHeight ?? 0
},
{ flush: 'post' }
)
</script>
<template>
<div>
<div ref="box" class="box">
<p v-for="row in props.rows" :key="row">{{ row }}</p>
</div>
<p>Content height: {{ height }}px</p>
</div>
</template>
<style scoped>
.box {
max-height: 6rem;
overflow-y: auto;
}
</style>Reveal an element, then focus it
This is the most common place beginners meet nextTick. The input is behind a v-if, so at the moment editing becomes true the element still does not exist and the template ref is null. Calling field.value.select() on that line throws; calling it with ?. does nothing at all, which is harder to notice. One await and the element is in the document.
<script setup lang="ts">
import { nextTick, ref, useTemplateRef } from 'vue'
const editing = ref(false)
const name = ref('Untitled')
const field = useTemplateRef<HTMLInputElement>('field')
async function startEditing() {
editing.value = true
await nextTick()
field.value?.select()
}
</script>
<template>
<div class="rename">
<input
v-if="editing"
ref="field"
v-model="name"
type="text"
@blur="editing = false"
/>
<button v-else type="button" @click="startEditing">{{ name }}</button>
</div>
</template>
<style scoped>
.rename input {
width: 10rem;
}
</style>
Discussion