In a Vue 3 component, why does measuring a list right after pushing an item return the old height, and how does nextTick() help?
answer
- the DOM is not patched on write
- writes queue a component job
- flush runs in a microtask
- await the flush, then measure
basics
~20 sVue 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 sWriting 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<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
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.
Explain that the write queues the component's update job and schedules a microtask flush, and that nextTick() returns that flush's promise.
Recognise staleness bugs in scroll, focus and measurement code, and know the edge cases: calling nextTick too early, and patched-but-not-painted DOM.
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