What is nexttick in Vue.js?
nextTick is Vue's utility that runs your code after the DOM has reflected the latest reactive state changes.
Theory
TL;DR
- Think of Vue's reactivity like a restaurant kitchen: orders (state changes) queue up and get cooked in a batch, then plated all at once;
nextTickwaits for the plates to land before you check the dining room - State change is instant; DOM update happens on the next microtask
nextTick= microtask (before browser paint);setTimeout(fn, 0)= macrotask (after paint)- Rule: if you read or manipulate the DOM right after a state change, use
nextTick
Quick example
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
const increment = async () => {
count.value++
console.log(count.value) // ✅ 1 - reactive state is instant
// DOM still shows "Count: 0" here
await nextTick()
console.log($refs.msg.textContent) // ✅ "Count: 1" - DOM caught up
}
</script>
<template>
<p ref="msg">Count: {{ count }}</p>
<button @click="increment">+1</button>
</template>Without nextTick, reading $refs.msg.textContent right after count.value++ returns stale data. The reactive value is ready, but the DOM is one tick behind.
Why Vue batches DOM updates
Vue uses Proxy to track dependencies during render. When state changes, Vue does not re-render the component immediately. Instead, it queues the update in a scheduler. Then, in the next microtask (via Promise.resolve().then()), it flushes that queue and patches the DOM in one pass.
This is by design. If you update five reactive values inside one function, you want one DOM update, not five. One reflow, one paint, smooth 60fps.
nextTick hooks into this same queue. When you await nextTick(), you wait for that flush to finish. The DOM is stable by the time your next line runs.
When to use
- Measure element after state change -
await nextTick(), thengetBoundingClientRect() - Focus input after conditional render - toggle
v-if, thenawait nextTick(), then.focus() - Scroll to new list item - push to array, then
await nextTick(), then.scrollIntoView() - Initialize third-party DOM library - set data, then
await nextTick(), thennew Chart(el) - Trigger animation on toggle - flip a flag, then
await nextTick(), then.classList.add('animate')
The pattern is always the same: state change first, nextTick second, DOM interaction third.
How it works internally
In Vue 3, the scheduler lives in @vue/runtime-core. When a watcher or computed fires, it adds a job to a global queue array. Vue then calls queueMicrotask() (or Promise.resolve().then() as a fallback) to schedule flushSchedulerQueue(). That function runs patch() on each queued component and mutates the real DOM.
nextTick either joins this same flush (if it is still pending) or schedules a callback right after it. That is why it is precise: it is not guessing with a timer, it is literally next in line in the microtask queue.
For comparison: setTimeout(fn, 0) is a macrotask. It runs after the browser has already painted. That means you can see a flash of old content before your code runs. nextTick avoids that entirely.
Common mistakes
1. Calling nextTick before the state change
await nextTick() // Nothing pending, resolves immediately
state.value = newVal // Change happens afterNo pending updates exist when nextTick runs, so it resolves right away. Your DOM read still catches stale data. State change must come first.
2. Awaiting nextTick inside a loop
for (let item of items) {
state.value = item
await nextTick() // Vue batches all of these anyway
}Vue batches updates across ticks. Awaiting inside a loop does not give you one DOM update per iteration - it just hammers the scheduler unnecessarily. Batch all state changes first, then one nextTick at the end.
3. Double nextTick (almost always unnecessary)
state.value = newVal
await nextTick() // DOM updated
await nextTick() // Queue is empty, resolves immediatelyThe second call resolves instantly because the queue is empty. One nextTick is enough. The only real edge case is when your first nextTick callback itself triggers a new reactive change that queues a second flush.
4. Using nextTick in SSR
On the server there is no DOM. nextTick becomes a no-op in server-side rendering environments like Nuxt. Wrap DOM-dependent logic in onMounted combined with nextTick to keep it client-only.
Real-world usage
- Element Plus (
ElMessage) - positions toast notifications after DOM insert usingnextTick - Vuetify (
v-menu) - auto-focuses menu content on open vianextTick - Quasar (
QTable) - awaitsnextTickbefore scrolling to the sorted row - Vue Test Utils / Vitest -
await nextTick()inside tests to sync DOM after state changes - Pinia plugins - some plugins await
nextTickfor DOM-driven store actions
I have seen the focus-after-render pattern in almost every production Vue app. It looks optional until you discover that half your modals have broken autofocus.
Follow-up questions
Q: What is the difference between nextTick and setTimeout(fn, 0)?
A: nextTick is a microtask and runs before the browser paints. setTimeout(fn, 0) is a macrotask and runs after paint. For DOM reads after state changes, nextTick is faster and more predictable.
Q: Does nextTick work in server-side rendering?
A: No. In SSR there is no DOM, so nextTick is a no-op. Put DOM-dependent code inside onMounted, and pair it with nextTick if you need to wait for the first render to flush.
Q: What is the difference between callback syntax and async/await?
A: nextTick(cb) registers a one-time callback. await nextTick() does the same but lets you chain code below it naturally. Both work; await is more readable in modern async functions.
Q: How does nextTick relate to watchEffect?
A: watchEffect runs synchronously on reactive change, before the DOM is updated. If you need to read the DOM inside a watchEffect, wrap that read in nextTick. Otherwise you get pre-patch values.
Q: When would you ever need two consecutive nextTick calls?
A: Almost never. A second nextTick after the first runs on an empty queue. The only real case: if your first nextTick callback triggers a new reactive change, which queues a second flush. Then a second nextTick waits for that second flush specifically.
Q: How would you implement a custom scheduler that batches like Vue but supports priorities - urgent jobs before normal ones?
A: Maintain a queue like { job, priority }. On flush, sort by priority descending, then run jobs in order using queueMicrotask. Urgent jobs (DOM reads, focus) get priority 1, normal effects get 0. Vue's own scheduler has a similar concept with pre-flush and post-flush watchers.
Examples
Auto-focus input after adding a todo
<script setup>
import { ref, nextTick } from 'vue'
const todos = ref([])
const newTodo = ref('')
const inputRef = ref(null)
const addTodo = async () => {
if (!newTodo.value.trim()) return
todos.value.push({ id: Date.now(), text: newTodo.value })
newTodo.value = ''
await nextTick()
inputRef.value.focus() // Input exists in DOM now
inputRef.value.select() // Select any remaining text
}
</script>
<template>
<input ref="inputRef" v-model="newTodo" @keyup.enter="addTodo" placeholder="Add todo...">
<ul>
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</ul>
</template>After todos.value.push(...), the new list item is not in the DOM yet. await nextTick() waits for Vue to patch the DOM, then .focus() and .select() work correctly.
Auto-scroll chat to latest message
<script setup>
import { ref, nextTick } from 'vue'
const messages = ref([])
const chatRef = ref(null)
const sendMessage = async (text) => {
messages.value.push({ id: Date.now(), text })
await nextTick()
// scrollHeight reflects the new message only after DOM patch
chatRef.value?.scrollTo({
top: chatRef.value.scrollHeight,
behavior: 'smooth'
})
}
</script>
<template>
<div ref="chatRef" style="height: 300px; overflow-y: auto;">
<p v-for="msg in messages" :key="msg.id">{{ msg.text }}</p>
</div>
<button @click="sendMessage('Hello!')">Send</button>
</template>Without nextTick, scrollHeight does not yet include the new message. You end up scrolling to the second-to-last message every time.
Measuring element dimensions after conditional render
<script setup>
import { ref, nextTick } from 'vue'
const visible = ref(false)
const boxRef = ref(null)
const boxSize = ref(null)
const toggle = async () => {
visible.value = !visible.value
if (visible.value) {
await nextTick()
// Element is mounted now, getBoundingClientRect() returns real values
boxSize.value = boxRef.value?.getBoundingClientRect()
console.log('Width:', boxSize.value?.width)
}
}
</script>
<template>
<button @click="toggle">Toggle</button>
<div v-if="visible" ref="boxRef" style="width: 200px; height: 100px; background: steelblue;" />
<p v-if="boxSize">Box width: {{ boxSize.width }}px</p>
</template>Before nextTick, when v-if was false, boxRef.value is null because the element does not exist in the DOM. After nextTick, it is mounted and fully measurable.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.