Vue
Pinia: управление состоянием
Корзина магазина нужна сразу в шапке (счётчик товаров), на странице товара (кнопка добавить) и на экране оформления (полный список). Передавать её через props сквозь десяток уровней компонентов - боль и хрупкость. Можно завести общий reactive-объект в модуле, но тогда любой компонент сможет молча мутировать его как угодно, и отследить, кто и когда изменил корзину, станет невозможно. Pinia даёт общее хранилище с понятной структурой: данные в state, производные значения в getters, изменения только через actions, и всё это с поддержкой devtools и типов.
- Корзина магазина, доступная из шапки, карточки товара и экрана оформления одновременно
- Состояние авторизации: текущий пользователь и его права, нужные множеству экранов и навигационным гвардам
- Глобальные настройки UI: тема, язык, открытые модалки, общие для всего приложения
- Кэш загруженных данных, чтобы не запрашивать одно и то же при возврате на экран
- Уведомления и тосты, которые любой компонент может добавить в общую очередь
Предварительные знания
- Композаблы: функция, возвращающая реактивное состояние и логику
- ref, reactive, computed и их поведение
- storeToRefs и идея сохранения реактивности при деструктуризации
Зачем Pinia вместо общего reactive
Простейший общий стейт - reactive-объект в модуле, импортируемый где нужно. Технически это работает: реактивность общая. Но у подхода нет дисциплины. Любой компонент мутирует объект напрямую, и когда корзина внезапно очистилась, найти виновника тяжело - менять её мог кто угодно. Нет единой точки изменений, нет истории, нет интеграции с инструментами отладки.
Pinia добавляет структуру поверх той же реактивности. Изменения проходят через actions - именованные методы, которые видны в devtools как события. Состояние централизовано в сторах, у каждого свой id. Появляются time-travel отладка, отслеживание мутаций, горячая замена модулей и строгая типизация. Реактивность та же, но с правилами и инструментами вокруг неё.
| Аспект | Общий reactive | Pinia |
|---|---|---|
| Точка изменений | Любой компонент напрямую | Только actions, видны в devtools |
| Отладка | console.log вручную | Devtools, time-travel, лог мутаций |
| Типизация | Ручная, легко рассинхронить | Выводится из стора автоматически |
| Жизненный цикл | Живёт вечно в модуле | effectScope, HMR, $dispose |
В чём главное преимущество Pinia над обычным общим reactive-объектом?
Setup-стор: defineStore с функцией
Pinia 3 предлагает два стиля стора, и setup-стор - наиболее близкий к Composition API. defineStore принимает id и setup-функцию, которая выглядит как обычный композабл: ref становится state, computed становится getter, обычная функция становится action. То, что функция вернёт, образует публичный интерфейс стора.
Соответствие прямое: ref - это реактивные данные стора, computed - производные значения, кэшируемые и пересчитываемые автоматически, функции - единственный санкционированный способ менять состояние. Перед подключением сторов приложение получает Pinia через app.use(createPinia()).
Setup-стор и есть применение идеи композабла к глобальному состоянию. Разница в том, что обычный композабл создаёт новый экземпляр на каждый вызов, а стор Pinia возвращает один общий экземпляр на всё приложение.
Чем в setup-сторе Pinia становятся ref, computed и обычная функция?
Использование стора в компоненте
В компоненте стор получают вызовом useCartStore() внутри setup. Возвращается один и тот же экземпляр для всех компонентов - синглтон на приложение, поэтому корзина в шапке и на странице оформления это буквально одно состояние. Через сам экземпляр доступны и данные, и getters, и actions.
Ловушка возникает при деструктуризации. Стор - это reactive-объект, поэтому const { count, total } = cart разорвёт реактивность, как и любая деструктуризация reactive. Решение знакомо из урока про утилиты: storeToRefs оборачивает state и getters в ref, сохраняя связь. Actions при этом берут прямо из стора - функции стабильны и в обёртке не нуждаются.
Распространённая ошибка - деструктурировать стор как обычный объект: const { count } = useCartStore(). Значение замёрзнет на первоначальном. Для state и getters всегда нужен storeToRefs, иначе реактивность теряется молча.
Как правильно деструктурировать state и getters из стора Pinia, не потеряв реактивность?
Связь с другими темами
Урок собирает воедино реактивность, композаблы и роутинг:
- Композаблы — Setup-стор Pinia по форме - это композабл с особым контрактом
- Утилиты реактивности — storeToRefs применяет toRefs к стору для безопасной деструктуризации
- effectScope — Стор живёт в собственной области эффектов, что обеспечивает их корректную остановку
Итог
- Pinia - официальное хранилище состояния Vue: общее реактивное состояние с понятной структурой и поддержкой devtools
- Setup-стор объявляется через defineStore(id, setupFn), где функция возвращает state (ref/reactive), getters (computed) и actions (функции)
- useStore вызывается внутри setup и возвращает один и тот же экземпляр стора для всех компонентов (синглтон на приложение)
- Деструктуризация стора теряет реактивность, поэтому state и getters достают через storeToRefs, а actions берут прямо из стора
- Pinia предпочтительнее обычного reactive-объекта: явный контракт изменений через actions, интеграция с devtools, типизация, HMR и effectScope
Связанные уроки
- vue-12-composables — Setup-стор пишется как композабл, поэтому нужно понимать саму идею композаблов
- vue-21-reactivity-utilities — storeToRefs - прямое применение toRefs для безопасной деструктуризации стора
- vue-22-effect-scope — Каждый стор Pinia живёт внутри effectScope, что объясняет корректную остановку его эффектов