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 — Загруженные данные тоже надо где-то хранить, и выбор хранилища следует тем же правилам
Архитектура состояния: компонент vs Pinia vs provide

0

1

Войти