Skip to main content

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; nextTick waits 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

vue
<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(), then getBoundingClientRect()
  • Focus input after conditional render - toggle v-if, then await 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(), then new 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

js
await nextTick() // Nothing pending, resolves immediately state.value = newVal // Change happens after

No 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

js
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)

js
state.value = newVal await nextTick() // DOM updated await nextTick() // Queue is empty, resolves immediately

The 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 using nextTick
  • Vuetify (v-menu) - auto-focuses menu content on open via nextTick
  • Quasar (QTable) - awaits nextTick before scrolling to the sorted row
  • Vue Test Utils / Vitest - await nextTick() inside tests to sync DOM after state changes
  • Pinia plugins - some plugins await nextTick for 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

vue
<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

vue
<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

vue
<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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?