Паттерны состояния: когда Pinia, когда composable, когда provide
Не всякое состояние требует Pinia. Учимся выбирать между локальным ref, composable, provide/inject и глобальным store.
Управление состоянием — это в первую очередь выбор области видимости: насколько широко состояние должно быть доступно и кто им владеет.
Частая ошибка новичка — тащить всё в Pinia «на всякий случай». Это раздувает глобальное пространство, усложняет тесты и размывает ответственность. У вас есть лестница инструментов от самого локального к самому глобальному — берите наименьший достаточный.
Почему это важно. Глобальное состояние — это, по сути, общая изменяемая переменная, видимая отовсюду. Чем его больше, тем труднее ответить на вопрос «кто и когда это поменял», тем легче поймать неожиданную связанность между несвязанными экранами и тем дороже писать изолированные тесты. Локальное же состояние самодокументируемо: оно живёт рядом с тем, кто им владеет. Поэтому правило простое — поднимайте состояние на уровень выше только тогда, когда этого реально требуют потребители.
Уровень 1: локальный ref
Если состояние нужно одному компоненту — открыт ли дропдаун, что в поле ввода — это просто ref в этом компоненте. Никаких composable и store. Глобализировать такое — антипаттерн: вы делаете приватную деталь видимой всему приложению.
<script setup>
import { ref } from 'vue'
const isOpen = ref(false) // живёт и умирает вместе с компонентом
</script>
Уровень 2: composable для переиспользуемой логики
Если одна и та же логика с состоянием нужна в нескольких компонентах, но каждому — свой экземпляр (свой таймер, своя позиция мыши, свой результат запроса), это composable. Помните: каждый вызов useX() создаёт независимое состояние.
function useToggle(initial = false) {
let state = initial
return {
toggle: () => { state = !state; return state },
get value() { return state },
}
}
const a = useToggle()
const b = useToggle(true)
console.log(a.toggle()) // независим от b
console.log(b.value)
console.log(a.value)
Вывод:
true true true
Видно, что a и b не влияют друг на друга — у каждого своё состояние. Это ключевое свойство composable: переиспользование логики, а не данных.
Уровень 3: общее состояние без Pinia
Иногда нужно одно общее состояние, но тянуть Pinia ради него — перебор. Тогда работает приём «shared composable»: объявляем ref вне функции, в области модуля. Поскольку модуль импортируется один раз, все компоненты получают один и тот же ref.
<script>
import { ref } from 'vue'
// ref вне функции — общий на всё приложение
const count = ref(0)
export function useSharedCounter() {
const inc = () => count.value++
return { count, inc } // тот же count у всех импортёров
}
</script>
Это легально и удобно для небольшого глобального состояния. Минус — нет devtools, нет структурированных actions, сложнее тестировать в изоляции (общий ref переносит своё значение между тестами, если его не сбрасывать). Ещё нюанс — SSR: модульный ref на сервере общий для всех запросов сразу, что в серверном рендеринге опасно утечкой данных между пользователями. Поэтому для сервера такой приём не годится без аккуратности. Как только состояние растёт или появляется SSR — переходите на Pinia, которая всё это учитывает.
Уровень 4: provide / inject
provide/inject решает другую задачу — не глобальность, а передачу вглубь дерева компонентов без «пробрасывания пропсов» через каждый уровень. Родитель «предоставляет» значение, любой потомок ниже его «внедряет».
<script setup>
// в компоненте-родителе
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<script setup>
// в любом потомке, как угодно глубоко
import { inject } from 'vue'
const theme = inject('theme', 'light') // второй аргумент — значение по умолчанию
</script>
Главное отличие от Pinia: provide/inject состояние привязано к поддереву. Два разных родителя могут предоставить разные значения под одним ключом — у каждого своя ветка. Это идеально для тем, локали формы, конфигурации виджета, передаваемой его внутренним частям.
Матрица выбора
| Ситуация | Инструмент |
| Состояние одного компонента | локальный ref |
| Переиспользуемая логика, у каждого свой экземпляр | composable |
| Маленькое общее состояние, без devtools | shared composable (ref в модуле) |
| Передача вглубь поддерева | provide/inject |
| Крупное глобальное состояние, devtools, async | Pinia |
Когда точно Pinia
Берите Pinia, когда состояние глобальное (нужно многим несвязанным частям), сложное (много полей, getters, async-actions) и вы хотите инструменты — отладку через devtools, плагины (персист), горячую перезагрузку. Корзина, авторизация, нотификации, кеш данных — кандидаты на store. Их объединяет то, что shared composable быстро упрётся в потолок организованности.
Как это работает под капотом
Все четыре подхода стоят на одной реактивности Vue (ref/reactive). Разница — в области владения. Локальный ref живёт в области setup компонента и собирается сборщиком мусора при размонтировании. ref в модуле живёт столько, сколько живёт модуль — то есть всё время работы приложения, поэтому он общий. provide кладёт значение в контекст экземпляра компонента, а inject идёт вверх по цепочке родителей до первого совпадения ключа — это и даёт привязку к поддереву. Pinia — надстройка, которая хранит реактивные store в едином синглтоне и добавляет devtools и плагины.
Частые ошибки
- Класть локальное состояние в Pinia «на будущее» — лишняя глобальность и сложность тестов.
- Путать
provide/injectс глобальным store — это передача вглубь поддерева, у разных веток значения разные. - Объявлять «общий»
refвнутри функции composable — тогда он НЕ общий, а у каждого свой; для shared его выносят в модуль. - Тащить Pinia в крошечный проект, где хватило бы shared composable.
- Не задавать значение по умолчанию в
inject— компонент сломается, если его используют вне нужного родителя.
Итоги
- Выбор инструмента — это выбор области видимости: берите наименьший достаточный.
- Одному компоненту — локальный
ref; переиспользуемой логике — composable. - Небольшое общее состояние — shared composable (
refв модуле), без накладных расходов Pinia. provide/inject— про передачу вглубь поддерева, а не про глобальность.- Pinia — для крупного, сложного глобального состояния, где нужны devtools, плагины и структура.