skip to content

Update Queue & Next Tick

Vue buffers state changes and flushes deduplicated component updates once per tick, so the DOM lags a write until nextTick resolves. Interviewers ask why the DOM is stale right after a change.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a Vue 3 component, why does measuring a list right after pushing an item return the old height, and how does nextTick() help?

level: juniorimportance: must knowfreq 75%

answer

  1. the DOM is not patched on write
  2. writes queue a component job
  3. flush runs in a microtask
  4. await the flush, then measure

basics

~20 s

Vue 3 does not patch the DOM when state changes; the write queues the component's update job, which runs in a microtask flush later. Awaiting nextTick() after the push waits for that flush, so the measurement sees the new item.

solid answer

~30 s

Writing to reactive state in Vue 3 only *schedules* work: the component's render effect is triggered, its update job is put on the scheduler queue, and a flush is scheduled as a microtask. The code after `items.value.push(item)` keeps running synchronously, so the `<ul>` still contains the old children and `offsetHeight` is the old value. `await nextTick()` resolves after the pending flush has re-rendered the component and patched the DOM, so measuring after it sees the new `<li>`. Two cautions: call `nextTick()` after the write, and remember it guarantees the DOM is updated, not that the browser has painted.

code

vue · 23 lines
vue
<script setup lang="ts">
import { ref, nextTick } from 'vue'

const items = ref<{ id: number; text: string }[]>([])
const listEl = ref<HTMLUListElement | null>(null)
let nextId = 0

async function add(text: string) {
  items.value.push({ id: nextId++, text })
  console.log(listEl.value!.offsetHeight) // old height: update job only queued

  await nextTick()
  console.log(listEl.value!.offsetHeight) // new height: DOM patched
  listEl.value!.lastElementChild?.scrollIntoView()
}
</script>

<template>
  <ul ref="listEl">
    <li v-for="item in items" :key="item.id">{{ item.text }}</li>
  </ul>
  <button @click="add('row')">Add</button>
</template>

go deeper

for a junior

Remember that Vue updates the DOM after your code finishes, not on each assignment, and that await nextTick() after the change is the way to read the new DOM.

for a middle

Explain that the write queues the component's update job and schedules a microtask flush, and that nextTick() returns that flush's promise.

for a senior

Recognise staleness bugs in scroll, focus and measurement code, and know the edge cases: calling nextTick too early, and patched-but-not-painted DOM.

for a principal

Treat measurement after state changes as a pattern to standardise in shared utilities, so teams stop sprinkling timers to paper over the flush.

## The scenario A component renders a list and, after adding an item, wants to measure the list — to scroll to the bottom, size a container, or position a tooltip next to the new row. The naive code writes and then reads on the next line: ```ts items.value.push({ id: nextId++, text }) console.log(listEl.value!.offsetHeight) // still the old height ``` The measurement is one item short. Nothing is broken: this is how Vue 3's scheduler is designed. ## What a write actually does When `items.value.push(...)` runs, the reactive proxy notifies every effect that read `items` — including the component's **render effect**, the effect that re-runs the render function. In Vue 3 that effect does not re-render immediately. Its scheduler calls `queueJob`, which: 1. puts the component's **update job** into the scheduler queue (once — a second trigger in the same tick is ignored); 2. if no flush is pending yet, schedules one with a resolved promise's `then`, which is a **microtask**. Control then returns to your handler, which is still running synchronously. The DOM has not been touched, so any layout read — `offsetHeight`, `getBoundingClientRect()`, `scrollHeight` — reflects the previous render. ## What nextTick() waits for `nextTick()` returns the promise of the currently scheduled flush if there is one. That promise resolves after the flush has: - run the queued update jobs, re-rendering each changed component and patching the DOM; - run the post-flush callbacks, such as `onMounted` for newly created child components, `onUpdated` hooks and template ref assignments. So code after `await nextTick()` sees the new `<li>` in the DOM, and a new child component inside it has already mounted. If no flush is pending, `nextTick()` returns an already-resolved promise and simply continues on the next microtask. | Read happens | DOM state | |---|---| | on the line after the write | old: the update job is only queued | | after `await nextTick()` | new: the flush has patched the DOM | | inside a `computed` that depends on `items` | old: a computed recalculates on read, it does not render | ## Getting it right - **Order matters.** Write first, then `await nextTick()`. Awaiting it *before* the write resolves against whatever was pending at that moment, not against your change. - **Use the promise or the callback.** `await nextTick()` and `nextTick(() => measure())` are equivalent; in the Options API the same function is `this.$nextTick`. - **Updated is not painted.** After `nextTick()` the DOM is patched and layout reads force a synchronous layout, so they are accurate — but the frame has not necessarily been painted. If you need the user to have seen the frame, that is a browser timing question, not Vue's. - **Several writes, one wait.** Pushing three items and then awaiting once is enough: all three writes land in the same flush. ## Why Vue works this way Buffering means a handler that changes ten pieces of state causes one render per affected component, not ten. The price is exactly this staleness window between write and flush, and `nextTick()` is the API that closes it. Interviewers ask this question because it separates candidates who picture Vue as synchronously patching the DOM on every assignment from those who know about the queue. ## Common wrong fixes - Replacing `push` with `items.value = [...items.value, item]`: a different kind of write, the same queued update. - Calling `triggerRef(items)`: it forces a trigger, which only queues the same job again. - Reading from a `computed`: computed values are recalculated on read and never touch the DOM.

  • In Vue 3, what does nextTick() do if no DOM update is pending when you call it?
    It returns an already-resolved promise, so the code after `await nextTick()` simply continues on the next microtask. It does not wait for any future change. That is why it must be called after the state write whose result you want to observe.
  • After `await nextTick()`, has a newly added child component in the list already run its onMounted hook?
    Yes. `onMounted` for components created during the flush is queued as a post-flush callback, and the flush runs those callbacks before its promise resolves. Code after `await nextTick()` therefore runs after the new child has mounted.

Like dropping letters in an outbox that is collected once at the end of your turn: the recipient's copy does not change while you are still writing, and nextTick() is waiting until the collection has been delivered before you check it.

saying these in an interview costs you the question

  • Vue patches the DOM synchronously on every ref assignment
  • Assigning a new array instead of push makes the DOM update immediately
  • nextTick() waits until the browser has painted the new frame
  • Awaiting nextTick() before the write also waits for that write
  • A computed that depends on the list gives the new DOM height
open as a page

In Vue 3, a click handler assigns three different refs that one component renders; how many times does that component re-render, and why?

level: middleimportance: must knowfreq 60%

basics

~20 s

Once. Each write triggers the component's render effect, but its update job is already marked as queued after the first, so the later triggers are ignored; the single microtask flush re-renders once with all three new values.

open as a page

In Vue 3, what does one scheduler flush run, in what order, and when does an awaited nextTick() resume relative to it?

level: middleimportance: should knowfreq 40%

basics

~20 s

A Vue 3 flush runs the main queue sorted by component id, with pre-flush watcher jobs just before their component's update, then the post-flush callbacks such as mounted and updated hooks; nextTick() returns that flush's promise, so it resumes after all of it.

open as a page

A Vue 3 app in development throws 'Maximum recursive updates exceeded'; what has the scheduler detected, and how do you track down the cause?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Vue 3's development scheduler counts each job's runs per flush and throws past 100: an effect keeps mutating state it depends on and re-queues itself. Find that write in the render, a hook or a watcher and make it converge.

open as a page