Skip to main content

Що таке nexttick у Vue.js?

nextTick - утиліта Vue, яка запускає твій код після того, як DOM відобразив поточний реактивний стан.

Теорія

TL;DR

  • Vue нагадує кухню ресторану: замовлення (зміни стану) накопичуються і готуються пакетом, потім подаються всі разом; nextTick чекає, поки страви дійдуть до столу, перш ніж перевіряти зал
  • Стан змінюється миттєво; DOM оновлюється в наступному мікротаску
  • nextTick = мікротаск (до перемальовування браузером); setTimeout(fn, 0) = макротаск (після перемальовування)
  • Правило: якщо потрібно читати або змінювати DOM одразу після зміни стану, використовуй nextTick

Швидкий приклад

vue
<script setup> import { ref, nextTick } from 'vue' const count = ref(0) const increment = async () => { count.value++ console.log(count.value) // ✅ 1 - реактивний стан вже змінився // DOM тут ще показує "Count: 0" await nextTick() console.log($refs.msg.textContent) // ✅ "Count: 1" - DOM наздогнав } </script> <template> <p ref="msg">Count: {{ count }}</p> <button @click="increment">+1</button> </template>

Без nextTick читання $refs.msg.textContent одразу після count.value++ повертає застарілі дані. Реактивне значення вже готове, але DOM відстає на один тік.

Чому Vue відкладає оновлення DOM

Vue використовує Proxy для відстеження залежностей під час рендеру. Коли стан змінюється, Vue не перерендерює компонент одразу. Замість цього він ставить оновлення в чергу планувальника, а потім у наступному мікротаску (через Promise.resolve().then()) обробляє всю чергу і оновлює DOM за один прохід.

Логіка проста. Якщо в одній функції змінити п'ять реактивних значень, DOM оновиться один раз, а не п'ять. Один reflow, одне перемальовування, стабільні 60 кадрів на секунду.

nextTick підключається до тієї ж черги. Коли ти await nextTick(), ти чекаєш завершення цього проходу. Після нього DOM гарантовано актуальний.

Коли використовувати

  • Виміряти елемент після зміни стану - await nextTick(), потім getBoundingClientRect()
  • Сфокусувати input після умовного рендеру - перемкнути v-if, потім await nextTick(), потім .focus()
  • Прокрутити до нового елемента списку - додати в масив, потім await nextTick(), потім .scrollIntoView()
  • Ініціалізувати сторонню DOM-бібліотеку - встановити дані, потім await nextTick(), потім new Chart(el)
  • Запустити анімацію після переключення - змінити флаг, потім await nextTick(), потім .classList.add('animate')

Патерн завжди однаковий: спочатку зміна стану, потім nextTick, потім робота з DOM.

Як це працює всередині

У Vue 3 планувальник живе в @vue/runtime-core. Коли спрацьовує watcher або computed, він додає завдання в глобальний масив-чергу. Vue потім викликає queueMicrotask() (або Promise.resolve().then() як fallback) щоб запланувати flushSchedulerQueue(). Ця функція запускає patch() для кожного компонента в черзі і мутує реальний DOM.

nextTick або приєднується до цього ж проходу (якщо він ще не завершився), або ставить колбек відразу після нього. Тому він точний: не гадає за таймером, а буквально стоїть наступним у черзі мікротасків.

Для порівняння: setTimeout(fn, 0) - це макротаск. Він запускається після того, як браузер вже перемалював сторінку. Ти можеш побачити миготіння старого вмісту до того, як твій код виконається. nextTick цього не допускає.

Типові помилки

1. Виклик nextTick до зміни стану

js
await nextTick() // Черга порожня, виконується одразу state.value = newVal // Зміна відбувається пізніше

Коли nextTick запускається, немає жодних очікуваних оновлень. Він вирішується миттєво, і DOM все одно повертає старе значення. Зміна стану має бути першою.

2. Очікування nextTick у циклі

js
for (let item of items) { state.value = item await nextTick() // Vue все одно пакетує всі зміни разом }

Vue пакетує оновлення між тіками. Очікування всередині циклу не дає одне DOM-оновлення на ітерацію - воно просто навантажує планувальник. Спочатку зроби всі зміни стану, потім один nextTick в кінці.

3. Подвійний nextTick (майже завжди зайвий)

js
state.value = newVal await nextTick() // DOM оновлено await nextTick() // Черга вже порожня, виконується миттєво

Другий виклик вирішується одразу, бо черга порожня. Одного nextTick достатньо. Єдиний виняток: якщо колбек першого nextTick сам провокує нову реактивну зміну, що ставить у чергу другий прохід.

4. Використання nextTick в SSR

На сервері немає DOM. У середовищах server-side rendering, таких як Nuxt, nextTick стає no-op. Обгортай DOM-залежну логіку в onMounted разом з nextTick, щоб вона виконувалась тільки на клієнті.

Де зустрічається в реальних проектах

  • Element Plus (ElMessage) - позиціонує toast-сповіщення після вставки в DOM через nextTick
  • Vuetify (v-menu) - автоматично фокусує вміст меню після відкриття через nextTick
  • Quasar (QTable) - чекає nextTick перед прокруткою до відсортованого рядка
  • Vue Test Utils / Vitest - await nextTick() в тестах для синхронізації DOM після змін стану
  • Pinia плагіни - деякі плагіни чекають nextTick для DOM-залежних дій зі стором

Особисто я бачив патерн "фокус після рендеру" в майже кожному production Vue-додатку. Це одна з тих речей, що виглядає необов'язковою, поки не виявляєш, що в половини модальних вікон не працює автофокус.

Питання на співбесіді

Q: Яка різниця між nextTick і setTimeout(fn, 0)?
A: nextTick - мікротаск, виконується до перемальовування браузера. setTimeout(fn, 0) - макротаск, виконується після. Для читання DOM після зміни стану використовуй nextTick - він швидший і передбачуваніший.

Q: Чи працює nextTick в server-side rendering?
A: Ні. В SSR немає DOM, тому nextTick є no-op. Поміщай DOM-залежний код в onMounted і комбінуй з nextTick, якщо треба дочекатися першого рендеру.

Q: Яка різниця між синтаксисом callback і async/await?
A: nextTick(cb) реєструє одноразовий колбек. await nextTick() робить те саме, але дозволяє природно писати код нижче. Обидва варіанти робочі; await читається краще в сучасних async-функціях.

Q: Як nextTick пов'язаний з watchEffect?
A: watchEffect виконується синхронно при зміні реактивного значення, до оновлення DOM. Якщо потрібно читати DOM всередині watchEffect, обгортай це в nextTick. Інакше отримаєш значення до патчингу.

Q: Коли може знадобитись два послідовних nextTick?
A: Майже ніколи. Другий nextTick після першого запускається на порожній черзі. Єдиний edge case: якщо колбек першого nextTick сам провокує нову реактивну зміну, яка ставить у чергу новий прохід. Тоді другий nextTick чекатиме саме цього другого проходу.

Q: Як би ти реалізував власний планувальник з підтримкою пріоритетів, як у Vue?
A: Зберігай чергу об'єктів { job, priority }. При flush сортуй за пріоритетом від вищого до нижчого і запускай через queueMicrotask. Термінові завдання (читання DOM, фокус) отримують пріоритет 1, звичайні ефекти - 0. У Vue подібна концепція реалізована через pre-flush і post-flush watchers.

Приклади

Автофокус input після додавання елемента списку

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 вже є в DOM inputRef.value.select() // Виділяємо текст, якщо є } </script> <template> <input ref="inputRef" v-model="newTodo" @keyup.enter="addTodo" placeholder="Додати завдання..."> <ul> <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li> </ul> </template>

Після todos.value.push(...) новий елемент списку ще не в DOM. await nextTick() чекає, поки Vue пропатчить DOM, і тоді .focus() спрацює правильно.

Автоматична прокрутка чату до останнього повідомлення

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 відображає нове повідомлення тільки після патчингу DOM 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('Привіт!')">Надіслати</button> </template>

Без nextTick scrollHeight ще не включає нове повідомлення. Прокрутка щоразу зупиняється на передостанньому елементі.

Вимірювання розмірів елемента після умовного рендеру

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() // Елемент вже змонтований, getBoundingClientRect() повертає реальні значення boxSize.value = boxRef.value?.getBoundingClientRect() console.log('Ширина:', boxSize.value?.width) } } </script> <template> <button @click="toggle">Перемкнути</button> <div v-if="visible" ref="boxRef" style="width: 200px; height: 100px; background: steelblue;" /> <p v-if="boxSize">Ширина блоку: {{ boxSize.width }}px</p> </template>

До nextTick, при v-if="false", boxRef.value дорівнює null - елемента просто немає в DOM. Після nextTick він змонтований і його можна вимірювати.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?