Олег Марков
Обновление компонента beforeUpdate во Vue
Введение
Хук жизненного цикла beforeUpdate в Vue (и Vue 2, и Vue 3 с опцией beforeUpdate в Options API) часто упоминают, но используют гораздо реже, чем mounted или created. Из‑за этого вокруг него остается много вопросов: в какой момент он срабатывает, что именно к этому времени уже обновлено, какие задачи с его помощью решать уместно, а какие приводят к «гонкам» и странным багам.
В этой статье мы подробно разберем, как работает beforeUpdate, что происходит с реактивными данными и DOM в этот момент, на каких примерах его применение действительно оправдано и как избежать типичных ошибок. Я буду показывать код с комментариями, чтобы вы могли сразу понять, что именно происходит на каждом этапе обновления компонента.
Под «компонентом» мы будем иметь в виду компонент Vue, а под «обновлением» — перерасчет виртуального DOM и синхронизацию с реальным DOM после изменения реактивных данных.
Жизненный цикл компонента и место beforeUpdate
Чтобы корректно использовать beforeUpdate, важно понимать, где он находится в цепочке жизненного цикла.
Упрощенно последовательность выглядит так (для уже смонтированного компонента):
- Меняются реактивные данные (например,
this.count++). - Vue планирует обновление компонента в следующем "тика" (итерации event loop).
- Перед реальным обновлением DOM вызывается хук
beforeUpdate. - Строится новый виртуальный DOM.
- Проводится сравнение старого и нового виртуального DOM (diff).
- Вносятся изменения в реальный DOM.
- Вызывается хук
updated.
Важно: к моменту beforeUpdate реактивные данные уже изменены, но реальный DOM еще не обновлен. Это центральный момент, который нужно запомнить.
Сравнение with created, mounted, updated и beforeUnmount
Давайте кратко сравним несколько ключевых хуков, чтобы вы увидели место beforeUpdate в общем контексте:
created
Доступны реактивные данные, но еще нет DOM (ни виртуального, ни реального). Невозможно работать с$el.mounted
Компонент отрисован первый раз, есть реальный DOM, но еще не происходило никаких последующих обновлений.beforeUpdate
Данные уже изменены, но DOM еще отображает старое состояние. Можно прочитать старое DOM-состояние, зная, какие данные стали новыми.updated
Данные изменены, виртуальный и реальный DOM синхронизированы, можно работать с новым DOM.beforeUnmount
Компонент скоро будет уничтожен, можно почистить ресурсы.
Смотрите, чтобы закрепить понимание, я покажу вам простой компонент и отмечу в логах, когда именно вызываются хуки:
export default {
data() {
return {
count: 0
}
},
created() {
console.log('created - count =', this.count)
},
mounted() {
console.log('mounted - DOM отрисован впервые')
},
beforeUpdate() {
console.log('beforeUpdate - count уже изменился и равен', this.count)
},
updated() {
console.log('updated - DOM уже отражает новое значение count')
},
methods: {
increment() {
// Меняем реактивные данные
this.count++
}
}
}
Когда вы вызовете increment, последовательность будет такая:
countувеличился.- Планируется обновление.
- Вызывается
beforeUpdate—countуже новый, DOM еще старый. - После обновления DOM вызывается
updated.
Сигнатура и объявление beforeUpdate
Во Vue 2 (Options API)
beforeUpdate объявляется как метод в объекте компонента:
export default {
data() {
return {
message: 'Привет'
}
},
beforeUpdate() {
// Здесь можно получить доступ к this.message
console.log('beforeUpdate - message =', this.message)
// DOM еще не обновлен
}
}
Комментарии к коду:
// beforeUpdate не принимает аргументов
// Внутри доступен this - экземпляр компонента
// Можно использовать this.$refs, но они ссылаются на старый DOM
Во Vue 3 Options API
В Options API Vue 3 сигнатура такая же:
export default {
data() {
return {
count: 0
}
},
beforeUpdate() {
// Хук работает аналогично Vue 2
console.log('beforeUpdate в Vue 3 - count =', this.count)
}
}
Во Vue 3 Composition API
Если вы используете Composition API, то аналогом beforeUpdate является функция onBeforeUpdate, которую вы вызываете внутри setup:
import { ref, onBeforeUpdate } from 'vue'
export default {
setup() {
const count = ref(0)
onBeforeUpdate(() => {
// Здесь вы используете переменные из setup
console.log('onBeforeUpdate - count =', count.value)
})
const increment = () => {
count.value++
}
return {
count,
increment
}
}
}
Комментарии:
// onBeforeUpdate принимает колбэк без аргументов
// Внутри доступ к реактивным переменным из setup
// DOM на момент вызова еще не обновлен
Что именно доступно в beforeUpdate
Хорошо понимать, что к этому моменту уже изменилось, а что еще нет. Давайте разложим по пунктам.
Реактивные данные
К моменту вызова beforeUpdate:
- все изменения в реактивных данных уже применены;
- вычисляемые свойства (
computed) пересчитаны; - все
watch(простые и глубокие) на эти данные либо уже отработали, либо запланированы к выполнению в зависимости от конфигурации.
Вы можете безопасно читать новые значения:
beforeUpdate() {
// Новое значение уже здесь
console.log('Новое значение count =', this.count)
}
DOM и $el
Вот здесь ключевой момент:
this.$el(корневой элемент компонента) еще соответствует старому DOM;- все дочерние элементы и содержимое шаблона тоже еще в старом состоянии.
Это удобно, когда вы хотите:
- сравнить старое DOM-состояние с новыми данными;
- измерить размеры элементов до обновления;
- сохранить какие‑то позиции/координаты, чтобы потом восстановить их после рендера.
Пример:
beforeUpdate() {
// Например, сохраняем текущий скролл до обновления списка
const container = this.$refs.listContainer
// Важно - DOM еще старый, поэтому скролл соответствует старому содержимому
this.prevScrollTop = container.scrollTop
}
Комментарии:
// this.$refs.listContainer должен быть задан в шаблоне как ref="listContainer"
// prevScrollTop - свойство, которое вы заранее объявили в data или setup
// Позже в updated вы сможете вернуть скролл на это значение
Когда вызывается beforeUpdate
Давайте разберем несколько ситуаций, чтобы у вас сложилась четкая картина.
Обновление из-за изменения data
Самый распространенный путь:
- Вы меняете
this.somePropв методе или реактивной переменной. - Vue помечает компонент как "грязный" (нуждается в обновлении).
- В следующем микротике/макротике (в зависимости от планировщика) Vue запускает обновление компонента.
- Вызывается
beforeUpdate.
methods: {
changeMessage() {
// Меняем реактивное состояние
this.message = 'Новое сообщение'
// beforeUpdate будет вызван один раз, даже если вы измените message несколько раз подряд синхронно
}
}
Vue по возможности объединяет несколько синхронных изменений в один цикл обновления.
Обновление из‑за изменения пропсов
Если родительский компонент меняет пропсы дочернему, у дочернего также вызывается beforeUpdate:
// Родитель
<Child :value="parentValue" />
// В родителе
this.parentValue = 42
В дочернем:
export default {
props: {
value: Number
},
beforeUpdate() {
// Здесь вы увидите уже новое значение пропса
console.log('Новое значение пропса value =', this.value)
}
}
Комментарии:
// beforeUpdate срабатывает при любом изменении пропсов
// Важно - пропсы уже обновлены, DOM - еще нет
Вложенные компоненты
Если обновляется родитель и меняются данные, которые передаются в дочерний компонент через пропсы, то:
- родительский компонент проходит через
beforeUpdate; - затем обновляются дочерние компоненты, где тоже вызывается
beforeUpdate(если действительно есть изменения, требующие перерасчета).
Порядок обычно такой:
beforeUpdateродителя;- обновление пропсов;
beforeUpdateдочерних;- изменение DOM (diff);
updatedродителя и дочерних.
Типичные сценарии использования beforeUpdate
beforeUpdate — не универсальный инструмент на все случаи. Но есть несколько задач, где он действительно полезен.
1. Сохранение состояния DOM перед обновлением
Классический пример — работа с прокруткой списков, позициями курсора, выделениями текста.
Давайте разберемся на примере компонента, который отображает большой список и подгружает новые элементы наверх. Задача: сохранить положение скролла при добавлении элементов, чтобы у пользователя не "скакал" экран.
<template>
<div
ref="listContainer"
style="height: 300px; overflow-y: auto;"
>
<div
v-for="item in items"
:key="item.id"
>
{{ item.text }}
</div>
</div>
</template>
export default {
data() {
return {
items: [],
scrollTopBeforeUpdate: 0,
scrollHeightBeforeUpdate: 0
}
},
methods: {
prependItems(newItems) {
// Добавляем новые элементы в начало массива
this.items = [...newItems, ...this.items]
// Vue сам вызовет beforeUpdate и updated
}
},
beforeUpdate() {
const container = this.$refs.listContainer
if (!container) return
// Сохраняем положение скролла и общую высоту до обновления DOM
this.scrollTopBeforeUpdate = container.scrollTop
this.scrollHeightBeforeUpdate = container.scrollHeight
},
updated() {
const container = this.$refs.listContainer
if (!container) return
// После обновления DOM считаем разницу высоты
const newScrollHeight = container.scrollHeight
const diff = newScrollHeight - this.scrollHeightBeforeUpdate
// Смещаем скролл так, чтобы пользователь остался на прежнем месте по содержимому
container.scrollTop = this.scrollTopBeforeUpdate + diff
}
}
Комментарии:
// beforeUpdate - момент, когда DOM старый, мы фиксируем его параметры
// updated - DOM уже новый, мы компенсируем изменение высоты, чтобы скролл визуально "не дернулся"
2. Аналитика и логирование изменений
Еще один сценарий: вы хотите логировать изменения состояния/DOM для отладки или аналитики. beforeUpdate дает возможность увидеть, что именно поменялось в данных, пока DOM еще старый.
beforeUpdate() {
// Например, логируем старое DOM-состояние и новые данные
console.log('beforeUpdate - новый count =', this.count)
console.log('beforeUpdate - DOM еще содержит старое значение:',
this.$el.textContent
)
}
Комментарии:
// Логи помогают понять порядок обновлений
// Полезно при неожиданных "скачках" интерфейса
3. Предварительная отмена или корректировка операций перед обновлением
Иногда в beforeUpdate удобно скорректировать часть состояния, чтобы не допустить нежелательного обновления или уменьшить его «размах».
Пример: у вас есть массив элементов, и вы не хотите обновлять DOM, если изменения фактически несущественные (например, порядок остался тем же). Вы можете в beforeUpdate провести дополнительную проверку, скорректировать данные, чтобы Vue вообще не пришлось изменять виртуальный DOM или чтобы изменилось минимальное количество узлов.
Уточню: полностью отменить обновление из beforeUpdate нельзя, но можно «откатить» часть данных, если логика компонента это допускает.
beforeUpdate() {
// Допустим, вы храните "новую" версию данных отдельно
if (this.pendingItems) {
// Проверим, действительно ли есть смысл обновлять список
if (arraysAreEqual(this.pendingItems, this.items)) {
// Если массивы эквивалентны, не будем применять изменения
this.pendingItems = null
} else {
// Применяем изменения перед обновлением
this.items = this.pendingItems
this.pendingItems = null
}
}
}
Комментарии:
// Это искусственный пример, но он показывает идею "подготовки" данных перед рендером
// Само обновление DOM все равно произойдет (хотя diff может ничего не изменить)
Чем beforeUpdate отличается от updated
Разница между beforeUpdate и updated — одна из самых важных тем, потому что неправильный выбор хука приводит к странным багам.
Смотрите на ключевые отличия.
Состояние данных и DOM
В
beforeUpdate:- данные уже новые;
- DOM еще старый.
В
updated:- данные уже новые;
- DOM синхронизирован с новыми данными.
Если вам нужно:
- сохранить данные о старом DOM →
beforeUpdate; - взаимодействовать с новым DOM (получить новые размеры, доступ к только что отрендеренным элементам) →
updated.
Пример различия на практике
Давайте посмотрим, как это видно в живом коде.
<template>
<div ref="box">
{{ count }}
</div>
<button @click="increment">+</button>
</template>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
beforeUpdate() {
const boxText = this.$refs.box.textContent.trim()
console.log('beforeUpdate - DOM внутри box =', boxText)
console.log('beforeUpdate - данные count =', this.count)
},
updated() {
const boxText = this.$refs.box.textContent.trim()
console.log('updated - DOM внутри box =', boxText)
console.log('updated - данные count =', this.count)
}
}
Когда вы нажимаете на кнопку:
в
beforeUpdateвы увидите:- DOM: старое значение
count(например, 0); - данные: новое значение
count(например, 1).
- DOM: старое значение
в
updatedвы увидите:- DOM: новое значение
count(1); - данные: новое значение (
count = 1).
- DOM: новое значение
Взаимодействие beforeUpdate с watch и computed
Давайте разберем, что происходит, если у вас в компоненте активно используются watch и вычисляемые свойства.
Порядок срабатывания watch и beforeUpdate
Обычно последовательность такова:
- Меняются реактивные данные.
- Пересчитываются
computed, которые зависят от этих данных. - Срабатывают
watch‑наблюдатели. - Планируется обновление компонента.
- Вызывается
beforeUpdate. - Обновляется DOM.
- Вызывается
updated.
Важный момент: отдельные watch могут быть синхронными или асинхронными (в зависимости от настроек и контекста), но чаще всего вы можете считать, что точно к beforeUpdate новые значения уже посчитаны.
Пример:
export default {
data() {
return {
value: 0
}
},
computed: {
doubled() {
return this.value * 2
}
},
watch: {
value(newVal, oldVal) {
console.log('watch value -', oldVal, '->', newVal)
}
},
beforeUpdate() {
console.log('beforeUpdate - value =', this.value, 'doubled =', this.doubled)
}
}
Комментарии:
// Когда вы меняете value, watch отрабатывает до beforeUpdate
// В beforeUpdate вы увидите уже пересчитанное doubled
Частые ошибки при использовании beforeUpdate
Теперь давайте разберем то, чего с beforeUpdate делать не стоит, или делать очень аккуратно. Я покажу проблемные шаблоны и более безопасные альтернативы.
1. Бесконечные циклы обновлений
Ошибку, с которой сталкиваются чаще всего, можно описать так: вы меняете реактивные данные внутри beforeUpdate, что снова планирует обновление, что снова вызывает beforeUpdate и так далее.
Пример плохой практики:
beforeUpdate() {
// Плохой пример - безусловное изменение реактивных данных
this.count++
}
Комментарии:
// Это приведет к бесконечному циклу обновлений
// Каждый вызов beforeUpdate снова меняет count, снова вызывает beforeUpdate
Как избежать:
- Менять данные в
beforeUpdateтолько при строго контролируемых условиях. - Добавлять проверки и "флаги" (переменные-состояния), чтобы изменение происходило один раз.
Пример безопасной реализации:
export default {
data() {
return {
count: 0,
adjustedOnce: false
}
},
beforeUpdate() {
// Допустим, вам нужно один раз скорректировать count
if (!this.adjustedOnce && this.count > 10) {
this.count = 10
this.adjustedOnce = true
}
}
}
Комментарии:
// adjustedOnce гарантирует, что изменение произойдет только один раз
// Цикл обновлений не станет бесконечным
2. Тяжелые вычисления в beforeUpdate
Еще одна ошибка: помещать в beforeUpdate тяжелую логику, которая выполняется при каждом обновлении компонента.
Примеры того, чего лучше избегать в beforeUpdate:
- сложные фильтрации и сортировки больших массивов;
- работа с сетью (HTTP‑запросы);
- масштабные преобразования данных.
Причина проста: beforeUpdate вызывается перед каждым обновлением, и если логика тяжелая, интерфейс начнет "лагать".
Что лучше сделать:
- вынести вычисления в
computed(они кэшируются и пересчитываются только при изменении зависимостей); - использовать
watchдля реакций на конкретные изменения и там выполнять операции.
3. Попытка работать с уже обновленным DOM
Иногда разработчики по ошибке считают, что beforeUpdate уже предоставляет доступ к новому DOM. В итоге они измеряют/модифицируют DOM в момент, когда он еще не соответствует данным.
Пример:
beforeUpdate() {
// Ошибка - вы ожидаете увидеть в DOM новое значение count
console.log('DOM:', this.$refs.box.textContent)
console.log('Данные:', this.count)
}
Как вы видели выше, в таких ситуациях DOM будет еще старым. Если вам нужен новый DOM, используйте updated или nextTick:
updated() {
// Здесь DOM уже синхронизирован с данными
console.log('Новый DOM:', this.$refs.box.textContent)
}
Практический пример — редактор текста с сохранением позиции курсора
Давайте соберем более цельный пример, где beforeUpdate действительно играет важную роль.
Задача: простой текстовый редактор, в который вы подставляете автодополнение (например, подставляете текст шаблона). При этом нужно сохранить позицию курсора, чтобы после обновления пользователь мог продолжить печатать.
Шаблон:
<template>
<textarea
ref="input"
v-model="text"
@keydown.tab.prevent="insertTemplate"
style="width: 100%; height: 150px;"
></textarea>
</template>
Логика:
export default {
data() {
return {
text: '',
cursorPositionBeforeUpdate: null
}
},
methods: {
insertTemplate() {
const textarea = this.$refs.input
if (!textarea) return
// Смотрите, я покажу вам, как получить позицию курсора до изменения текста
const start = textarea.selectionStart
const end = textarea.selectionEnd
const template = '[template]'
// Формируем новый текст с подстановкой шаблона
this.text =
this.text.slice(0, start) +
template +
this.text.slice(end)
// Сохраняем позицию, куда хотим вернуть курсор
this.cursorPositionBeforeUpdate = start + template.length
}
},
beforeUpdate() {
// Здесь нам DOM еще не нужен, позиции уже сохранены в cursorPositionBeforeUpdate
// Можно было бы измерять что-то еще, если нужно
},
updated() {
// Теперь DOM обновлен, можно выставить курсор в нужную позицию
const textarea = this.$refs.input
if (!textarea) return
if (this.cursorPositionBeforeUpdate != null) {
textarea.setSelectionRange(
this.cursorPositionBeforeUpdate,
this.cursorPositionBeforeUpdate
)
// Сбросим сохраненную позицию
this.cursorPositionBeforeUpdate = null
}
}
}
Комментарии:
// В insertTemplate мы меняем реактивные данные text
// После этого Vue вызовет beforeUpdate, а затем updated
// Мы используем beforeUpdate только как часть цикла — вся полезная работа с DOM идет в updated
// Важно понимать, что сохранение позиции курсора выполняется ДО изменения text, а восстановление — ПОСЛЕ обновления DOM
Здесь beforeUpdate сам по себе почти ничего не делает, но он важен как часть цикла "beforeUpdate → updated", когда вы мыслите в терминах "до обновления DOM" и "после".
Особенности использования beforeUpdate в Composition API
С onBeforeUpdate в Composition API подход немного отличается концептуально, хотя суть та же. Вы больше работаете с замыканиями и реактивными примитивами.
Пример: компонент, который отслеживает старое и новое значение списка для отладки.
<template>
<ul ref="list">
<li v-for="item in items" :key="item.id">
{{ item.label }}
</li>
</ul>
</template>
import { ref, onBeforeUpdate, onUpdated } from 'vue'
export default {
setup() {
const items = ref([
{ id: 1, label: 'A' },
{ id: 2, label: 'B' }
])
const listRef = ref(null)
const previousDomSnapshot = ref([])
onBeforeUpdate(() => {
const listEl = listRef.value
if (!listEl) return
// Давайте посмотрим, что было в DOM до обновления
const children = Array.from(listEl.children)
previousDomSnapshot.value = children.map((el) =>
el.textContent.trim()
)
console.log('onBeforeUpdate - старый DOM списка:', previousDomSnapshot.value)
})
onUpdated(() => {
const listEl = listRef.value
if (!listEl) return
const children = Array.from(listEl.children)
const newSnapshot = children.map((el) =>
el.textContent.trim()
)
console.log('onUpdated - новый DOM списка:', newSnapshot)
})
const shuffle = () => {
// Перемешиваем элементы для демонстрации
items.value = [...items.value].sort(() => Math.random() - 0.5)
}
return {
items,
listRef,
shuffle
}
}
}
Комментарии:
// onBeforeUpdate и onUpdated позволяют явно мыслить категориями "до" и "после"
// Вы используете ref для хранения DOM‑ссылок и "снимков" состояния для отладки
Когда beforeUpdate действительно нужен, а когда нет
Чтобы завершить основную часть, полезно сформулировать общие рекомендации.
Когда использовать beforeUpdate уместно
Нужно зафиксировать состояние DOM до того, как оно изменится:
- высота, ширина, позиция элементов;
- положение скролла;
- текущее содержимое.
Вы реализуете сложные визуальные эффекты:
- анимации переходов между состояниями;
- сравнение старого и нового содержимого для "диффа".
Вам важно понимать, какие именно изменения происходят:
- отладка рендеринга;
- логирование для анализа производительности.
Когда лучше обойтись без beforeUpdate
Нужно просто отреагировать на изменение данных:
- используйте
watchилиcomputed.
- используйте
Нужно работать с новым DOM:
- используйте
updatedилиnextTick.
- используйте
Нужно выполнить одноразовую инициализацию:
- используйте
mounted.
- используйте
Нужно очищать ресурсы:
- используйте
beforeUnmount/onBeforeUnmount.
- используйте
Заключение
beforeUpdate — это хук, который занимает четко определенное место в жизненном цикле компонента: он вызывается после изменения реактивных данных, но перед обновлением реального DOM. Это делает его удобным инструментом для задач, где важно зафиксировать старое состояние DOM, сопоставить его с новыми данными, подготовить вычисления или сохранить пользовательский контекст (например, скролл или позицию курсора) перед перерисовкой.
Однако из‑за своей "пограничной" природы beforeUpdate легко использовать не по назначению: вызывать в нем тяжелые операции, менять данные без защитных проверок и попадать в бесконечные циклы обновлений или пытаться работать с DOM так, как будто он уже отражает новые данные. Чтобы избежать этих проблем, полезно придерживаться следующих принципов:
- использовать
beforeUpdateдля чтения старого DOM и новых данных; - не выполнять в нем тяжелую бизнес‑логику;
- не менять реактивные данные без жесткого контроля;
- для работы с уже обновленным DOM предпочитать
updatedиnextTick.
Если вы будете воспринимать beforeUpdate как "точку наблюдения и подготовки" перед обновлением, а не как универсальное место для произвольных действий, этот хук станет полезным и предсказуемым инструментом в ваших Vue‑компонентах.
Частозадаваемые технические вопросы по теме статьи и ответы на них
Можно ли из beforeUpdate полностью отменить обновление компонента
Отменить обновление напрямую нельзя. Vue уже запланировал обновление, и хук вызывается в ходе этого процесса. Вы можете в beforeUpdate скорректировать данные так, чтобы diff виртуального DOM не привел к изменениям (например, вернуть старые значения), но сам цикл обновления все равно завершится. Если нужно условно не обновлять часть шаблона, используйте вычисляемые свойства, v-if или разделение на дочерние компоненты.
Как узнать в beforeUpdate старые значения реактивных данных
В beforeUpdate у вас уже есть только новые значения. Если вам нужны старые, сохраните их заранее в watch или в месте, где вы меняете данные. Например, используйте watch(value, (newVal, oldVal) => { this.prevValue = oldVal }), а затем читайте this.prevValue в beforeUpdate. Прямого доступа к "доизмененному" состоянию из самого хука нет.
Вызывается ли beforeUpdate при изменении только дочернего компонента
Если меняются только данные, локальные для дочернего компонента, beforeUpdate родителя не вызывается. Родитель обновится только если изменились его собственные реактивные данные или пропсы. Поэтому, если вы хотите отслеживать изменения исключительно в дочернем, используйте beforeUpdate/onBeforeUpdate внутри самого дочернего компонента.
Можно ли использовать async/await в beforeUpdate
Да, можно объявить beforeUpdate как async‑функцию, но это не задержит сам процесс обновления DOM. Vue не ждет завершения промиса: хук считается "отработавшим" сразу после вызова, а асинхронный код продолжает выполняться параллельно. Поэтому не рассчитывайте, что await сможет "приостановить" обновление; для подобных задач используйте логику управления данными, а не ожидание внутри хука.
Как ограничить количество вызовов логики в beforeUpdate при частых изменениях
Если данные меняются часто (например, при вводе текста) и beforeUpdate вызывается слишком много раз, оберните тяжелую часть логики в debounce или throttle. Например, создайте в mounted функцию с _.debounce или requestAnimationFrame и вызывайте ее внутри beforeUpdate. Это позволит выполнять тяжелую часть кода реже, сохранив при этом возможность пользоваться моментом "до обновления DOM".
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев