Vue
Архитектура состояния: компонент vs Pinia vs provide
Команда начала с модалки, у которой был один локальный флаг isOpen. Через полгода этот флаг переехал в глобальный стор 'на всякий случай', и теперь любое открытие модалки триггерит ре-рендер половины дашборда, а тесты компонента требуют поднять весь Pinia. Перебор с централизацией стоит дороже, чем кажется. Vue даёт три уровня хранения состояния, и выбор между ними это инженерное решение, а не вкус: локальный ref, provide/inject на поддерево и Pinia на всё приложение.
- Форма с черновиком: значения полей живут локально в компоненте, в стор уходит только итог при отправке
- Тема оформления: provide на корне раздаёт light/dark всему поддереву без prop drilling
- Корзина и авторизация: общие на всё приложение, поэтому им место в Pinia, а не в компоненте
- Таблица с сортировкой: состояние сортировки локальное, его не выносят в глобальный стор
- Виджет-конструктор: контекст текущего виджета раздаётся через provide дочерним полям редактора
Предварительные знания
- Локальное состояние компонента через ref и reactive
- Передача данных через props и события emit от ребёнка к родителю
- Базовое понимание Pinia: что такое стор и зачем он нужен
Локальное состояние как дефолт
Состояние, которое нужно одному компоненту и его шаблону, держат локально в ref или reactive. Это самый дешёвый вариант: нет зависимостей, компонент тестируется в изоляции, при размонтировании состояние исчезает вместе с ним. Большая часть UI-состояния (открыта ли модалка, что введено в поле, какая вкладка активна) именно такая.
Если состояние нужно соседнему компоненту, первый шаг это не глобальный стор, а подъём состояния к общему родителю (lifting state up) и передача вниз через props. Стор нужен, когда такой подъём становится prop drilling через много уровней.
Вопрос для проверки: кто читает это состояние? Если ответ 'только этот компонент и его дети через props', оно локальное. Глобальный стор тут добавит связанность без выгоды.
Какое состояние логичнее всего держать в локальном ref компонента?
provide/inject для поддерева
provide/inject раздаёт состояние всем потомкам без передачи через каждый промежуточный компонент. Родитель вызывает provide(key, value), любой потомок на любой глубине берёт inject(key). Это решает prop drilling для данных, привязанных к конкретной ветке дерева: тема внутри настроек, контекст формы для её полей, текущий элемент в редакторе.
- provide/inject — Состояние привязано к ветке дерева. Доступно только потомкам провайдера. Несколько независимых поддеревьев получают каждое свой экземпляр
- Pinia — Состояние глобальное, синглтон на приложение. Доступно из любого компонента независимо от его места в дереве и от навигации
provide/inject создаёт неявную связь: потомок зависит от предка, которого не видно в его props. Поэтому ключи типизируют через InjectionKey и проверяют inject на undefined, выбрасывая понятную ошибку, а не молча получая сломанное состояние.
Чем provide/inject отличается от Pinia по области видимости состояния?
Pinia для глобального состояния
Pinia подходит для состояния, которое общее на всё приложение и переживает навигацию: авторизованный пользователь, корзина, настройки интерфейса, кэш справочников. Такие данные читают несвязанные ветки дерева, и привязывать их к компоненту или поддереву искусственно. Стор это синглтон, он доступен из любого setup и из роутера.
| Состояние | Уровень | Почему |
|---|---|---|
| Открыта ли модалка | Локальный ref | Нужно одному компоненту |
| Значения полей формы | Локальный ref | Живут в форме, в стор уходит результат |
| Тема, локаль внутри секции | provide/inject | Раздаётся поддереву без prop drilling |
| Авторизация, токен | Pinia | Читают роутер, API-клиент, много компонентов |
| Корзина, настройки | Pinia | Общие на приложение, переживают навигацию |
Признак, что данные пора вынести в Pinia: одни и те же данные нужны компонентам из разных веток дерева, и передавать их через общего предка означало бы прокидывать props через много промежуточных слоёв, которым эти данные не нужны.
Глобальность не делает Pinia складом для всего. Локальное UI-состояние, попавшее в стор, добавляет ре-рендеры и связанность. В Pinia держат то, что действительно разделяется, а не то, что когда-нибудь может понадобиться.
Какой признак говорит, что состояние пора вынести в Pinia?
Цена преждевременной централизации
Соблазн вынести любое состояние в глобальный стор 'на будущее' дорого обходится. Лишние ре-рендеры: подписка на глобальный стор пересчитывает больше, чем нужно. Усложнённые тесты: компонент с локальным ref тестируется сам по себе, а компонент, завязанный на стор, требует поднять Pinia в каждом тесте. Связанность: модули начинают знать друг о друге через общий стор, который им не нужен.
- Локальный ref — Тест монтирует компонент и проверяет поведение. Ноль внешних зависимостей. Состояние умирает вместе с компонентом, утечек нет
- Преждевременный стор — Каждый тест поднимает Pinia, сбрасывает состояние между прогонами, мокает зависимости. Состояние живёт глобально и может протечь между экранами
Практичный путь: начинать с локального состояния, поднимать к родителю когда понадобилось соседу, переходить на provide для поддерева и на Pinia только когда данные реально разделяются на всё приложение. Рефакторинг из локального в стор дешевле, чем выпутывание лишнего глобального состояния обратно.
Правило по умолчанию: состояние держат настолько локально, насколько возможно, и поднимают на уровень выше только под доказанную нужду, а не под гипотетическую.
Почему вынос локального UI-состояния в глобальный стор 'на всякий случай' считается антипаттерном?
Связь с другими темами
Урок про выбор хранилища состояния. Дальше курс показывает откуда состояние берётся и как масштабируется:
- Pinia: паттерны и SSR — Глубокие приёмы стора, которые применяют когда выбор пал на Pinia
- Загрузка данных — Серверные данные тоже требуют решения где их держать после фетча
Итог
- Локальный ref/reactive это дефолт: состояние нужно одному компоненту и его шаблону, проще всего и без зависимостей
- provide/inject раздаёт состояние поддереву без prop drilling: тема, локаль, контекст формы или редактора
- Pinia для состояния, общего на всё приложение и переживающего навигацию: авторизация, корзина, настройки
- Преждевременная централизация дорогая: лишние ре-рендеры, усложнённые тесты, связанность ради гипотетической нужды
- Признак что пора в Pinia: одни данные нужны несвязанным веткам дерева, а prop drilling стал болезненным
Связанные уроки
- vue-28-pinia-patterns — Решение в пользу Pinia опирается на знание её паттернов: композиция, плагины, storeToRefs
- vue-30-data-fetching — Загруженные данные тоже надо где-то хранить, и выбор хранилища следует тем же правилам