Пока во Vue-проекте пять экранов и один разработчик, состояние живёт спокойно: данные передаются через props, события всплывают наверх, всё работает. Но как только приложение вырастает до нескольких десятков экранов, в команду приходит второй разработчик, а от бизнеса звучит «сделайте, чтобы корзина и уведомления были доступны из любого места», — эта схема начинает трещать по швам. Цепочки props тянутся на 4–6 уровней вглубь, события эмитятся через полприложения, а «глобальные» переменные превращают отладку в детектив. Разбираем, как навести порядок с помощью Pinia, composables и типизации — и почему архитектура состояния напрямую влияет на скорость разработки и стоимость поддержки.
Когда props и events перестают справляться
Props — отличный механизм для локальной связи «родитель — ребёнок», но в большом приложении его начинают использовать для передачи данных, нужных всему проекту. Возникает props drilling: поле профиля пользователя прокидывается через пять промежуточных компонентов, которые сами эти данные не используют, а служат прокладками. Каждый такой уровень — это лишний ре-рендер, лишний файл при рефакторинге и лишнее место для бага. В интернет-магазине, который мы принимали на поддержку, добавление одного поля в профиль требовало правок в девяти файлах — просто чтобы «протащить» значение вниз. Обратная связь через цепочку $emit усугубляет картину: событие всплывает через те же пять уровней, и ни один промежуточный компонент не понимает, зачем оно ему. Provide/inject решает проблему частично, но такое связывание плохо отслеживается, и при бесконтрольном использовании получается тот же хаос, только неявный.Pinia: официальный стандарт для Vue 3
С 2022 года команда Vue официально рекомендует Pinia как решение для управления состоянием — во всех новых проектах мы используем именно её. Библиотека весит около 1 КБ в сжатом виде, не требует мутаций, как когда-то Vuex, поддерживает модульность и из коробки работает с TypeScript, SSR и DevTools с историей изменений. Стор создаётся черезdefineStore и подключается в любом компоненте одной строкой — без прокидывания props и эмитов. Главное преимущество Pinia — независимость сторов: корзина, пользователь и каталог живут в отдельных файлах, тестируются по отдельности и не тянут за собой весь мир. Для бизнеса это предсказуемость: новому разработчику нужен день, чтобы понять, где что лежит, вместо недели археологии по чужому коду.Как разбить состояние на сторы и не создать монстра
Самая частая ошибка при внедрении Pinia — свалить всё в один «общий» стор. Правила, которые мы применяем в проектах:- Один стор — одна предметная область. user, cart, catalog, notifications. Стор на 500 строк — сигнал к декомпозиции.
- В сторы — только разделяемое состояние. Если данные нужны минимум двум несвязанным частям приложения — им место в сторе. Открыт ли дропдаун — состояние компонента, а не глобальный флаг.
- Не храните производные данные. Всё, что вычисляется из состояния (итог корзины, количество товаров), оформляйте как getters — иначе рассинхрон гарантирован.
- Действия — в actions. Запросы к API и бизнес-логика живут в сторе, а не в компонентах, чтобы их можно было переиспользовать и тестировать.
- Серверное состояние — отдельно. Данные с бэкенда требуют стратегии обновления и инвалидации; если их много, посмотрите на TanStack Query в связке с Pinia.
Composables: когда глобальный стор избыточен
Не всё состояние должно быть глобальным. Composables — функции вида usePagination или useFormValidation — позволяют упаковать реактивную логику вместе с её состоянием и переиспользовать точечно. Работает простое правило: состояние нужно одной ветке компонентов — делаем composable, состояние нужно всему приложению — делаем стор Pinia. Важный нюанс: если ref объявлен внутри функции, каждый вызов composable создаёт независимое состояние — удобно для двух независимых таблиц на одной странице; если на уровне модуля — состояние становится общим синглтоном. Composables также удобны как фасад над сторами: компонент вызывает useCart(), а внутри прячутся стор, валидация и запросы к API. Так публичный интерфейс остаётся компактным, а логика не расползается по компонентам.Типизация: страховка при росте команды
На проекте с одним разработчиком TypeScript кажется необязательной роскошью. На проекте с тремя и более — это инструмент, который экономит недели. Типизированный стор означает, что у состояния есть интерфейс, у actions — типизированные аргументы, а у ответов API — описанные модели. Практический эффект: переименование поля занимает минуты, потому что компилятор показывает все места использования, вместо того чтобы пользователи нашли баг «cart.total is undefined» в продакшене. Строгий режим (strict: true) — обязательное условие: половинчатая типизация даёт ложное чувство безопасности. И следите, чтобы в коде не появлялся any — он отключает все проверки ровно там, где они нужнее всего.Чек-лист типизации сторов и composables
Что мы делаем в каждом Vue-проекте, чтобы типизация реально работала, а не оставалась декларацией:- Включаем strict: true в tsconfig и запрещаем any правилом @typescript-eslint/no-explicit-any.
- Описываем интерфейс state для каждого стора и модели ответов API — вручную или генерацией из OpenAPI-схемы бэкенда.
- Типизируем аргументы actions и возвращаемые значения getters.
- Для ссылок с отложенной загрузкой указываем тип явно: ref<User | null>(null) вместо ref(null) — иначе TypeScript выведет неверный тип.
- Запускаем vue-tsc в CI, чтобы ошибка типов блокировала сборку, а не всплывала на ревью.
Ошибки, которые мы регулярно встречаем в чужом коде
Когда проект приходит к нам на доработку или поддержку, картина повторяется с точностью до деталей. Типичные антипаттерны:- Мега-стор на всё приложение. Конфликты в git, невозможность тестировать, страх что-либо поменять.
- Дублирование состояния. Одни данные живут в сторе, в props и ещё в локальном ref — и рано или поздно расходятся.
- Запросы к API внутри компонентов. Логика не переиспользуется, ответ некуда положить, компонент разрастается до 800 строк.
- «Глобальные» переменные в window. Нет реактивности, нет DevTools, нет типов — и утечки памяти в придачу.
- Мутация props вместо событий. Vue предупреждает об этом в консоли, но предупреждения игнорируют — до первого трудновоспроизводимого бага.
Архитектура состояния — это не про моду, а про деньги. Правильно организованное состояние означает: новая функция добавляется за дни, а не за недели, новый разработчик выходит на продуктив за пару дней, а правка в одном месте не ломает пять других. Рабочая формула для большого Vue-приложения: Pinia — для состояния, разделённого между несвязанными частями приложения, composables — для переиспользуемой локальной логики, TypeScript — чтобы всё это не развалилось при росте команды. Если ваш проект уже тонет в props и событиях — это чинится поэтапным рефакторингом, обычно за 2–4 недели без остановки разработки. А если проект только стартует — заложите эту основу сразу: переделывать потом всегда дороже.

