Роутер, Pinia и жизненный цикл
Вопрос
Чем Pinia отличается от Vuex и какие хуки меняются при переходе на Vue 3?
Короткий ответ
Pinia — штатное хранилище для Vue 3. Нет мутаций: состояние меняют в действиях или напрямую, и это уже отслеживается. Сторы — функции defineStore, их можно вызывать внутри setup. Devtools и типы выводятся из состояния без отдельного модуля мутаций, как в Vuex.
Vuex 4 запускается на Vue 3, но новый код пишут на Pinia. Во Vuex изменение состояния шло через мутации, чтобы лента devtools была явной. В Pinia ту же ленту строят без этого слоя.
Vue Router 4 создаётся через createRouter. История — createWebHistory(), а не mode: 'history'. Catch-all записывают как /:pathMatch(.*)*, параметр * больше не работает. В гардах можно возвращать false или маршрут, а не только вызывать next.
Хуки экземпляра:
| Vue 2 | Vue 3 |
|---|---|
beforeDestroy | beforeUnmount |
destroyed | unmounted |
beforeCreate / created | код в setup до монтирования |
onMounted в Composition API соответствует mounted. Парный onUnmounted снимает слушатели, таймеры и подписки.
Разбор
Данные маршрута читают через useRoute(). Для перехода — useRouter(). Следить за параметром нужно явно: компонент может переиспользоваться при смене :id, и onMounted второй раз не вызовется. Тогда берут watch(() => route.params.id, load).
В Pinia стор не деструктурируют напрямую, если нужна реактивность. storeToRefs(store) оставляет реактивными поля состояния и computed, а действия забирают с самого стора.
const store = useCartStore()
const { items } = storeToRefs(store)
store.add(product)Порядок гардов при переходе с маршрута A на B: beforeRouteLeave у старого компонента, глобальный beforeEach, beforeRouteUpdate если компонент переиспользуется, beforeEnter в конфиге, beforeRouteEnter у нового (там ещё нет this), затем beforeResolve и после подтверждения afterEach. Проверку входа чаще вешают на beforeEach и to.meta.requiresAuth.
Навигационный гард, который ходит в сеть, лучше делать с понятным отказом: редирект на логин или отмена перехода. Тихий next(false) без причины сложно отлаживать.
$subscribe следит за любым изменением стора, например чтобы записать корзину в localStorage. $onAction видит имя, аргументы, успешный конец и ошибку экшена. Плагин Pinia расширяет все сторы сразу, типичный пример — pinia-plugin-persistedstate.
На что смотрят на senior
Стор не обязан знать про компоненты. Если действие стора принимает экземпляр роутера «чтобы само редиректнуло», границу размыли. Редирект остаётся в гарде или в обработчике экрана, стор возвращает результат.
Кэш страницы через KeepAlive сохраняет экземпляр: onUnmounted не вызовется, пока компонент не уберут из кэша. Для «ушёл с экрана, но компонент жив» есть onDeactivated / onActivated. Это частый источник живых таймеров на скрытой вкладке.
Типичные ошибки
- Писать мутации Pinia по привычке из Vuex. Отдельного типа мутаций нет.
- Деструктурировать стор и терять обновления шаблона.
- Полагаться на
onMountedпри смене param на том же компоненте маршрута. - Забывать переименовать
beforeDestroyпри переносе компонента на Vue 3.
Задача
Маршрут /orders/:id использует один и тот же компонент. При переходе с /orders/1 на /orders/2 onMounted не вызывается, на экране остаётся первый заказ. Чем догрузить второй?
Показать ответ
Компонент переиспользуется, монтирование один раз. Нужен watch(() => route.params.id, load).
beforeRouteUpdate решает ту же точку в Options API. Гард beforeEnter на такой переход между двумя id одного маршрута не рассчитывайте как на единственное место: экземпляр уже жив.