Pinia вглубь: stores, getters, actions, плагины
Pinia — официальная библиотека управления состоянием для Vue: разбираем её устройство до плагинов.
Pinia — хранилище глобального состояния для Vue 3, построенное на Composition API: state, getters и actions без шаблонного кода Vuex.
Composable отлично переиспользует логику, но каждый его вызов создаёт новый экземпляр состояния. Когда нужно одно общее состояние на всё приложение — корзина, текущий пользователь, тема — нужен store. Pinia даёт его с типобезопасностью, поддержкой devtools и горячей перезагрузкой.
Определение store
Store объявляют функцией defineStore с уникальным id. Есть два стиля: options (похож на Options API) и setup (как <script setup>). Начнём с options — он нагляднее.
<script>
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
}),
getters: {
count: (state) => state.items.length,
total: (state) => state.items.reduce((s, i) => s + i.price, 0),
},
actions: {
add(product) {
this.items.push(product)
},
clear() {
this.items = []
},
},
})
</script>
State, getters, actions
State — это функция, возвращающая начальные данные (функция, чтобы у каждого приложения был свежий объект). Getters — вычисляемые значения, по сути computed поверх state; они кешируются и пересчитываются только при изменении зависимостей. Actions — методы, меняющие state; внутри них this указывает на сам store. Actions могут быть асинхронными — это естественное место для запросов к API.
Геттеры умеют опираться друг на друга и принимать аргументы через возврат функции — например, itemById вернёт (id) => state.items.find(i => i.id === id). Такой «фабричный» геттер уже не кешируется (он параметризован), и это нормально. А вот обычный total кешируется: пока корзина не изменилась, повторное чтение cart.total не пересчитывает сумму заново — ровно как computed в компоненте.
Setup-стиль
Тот же store в setup-стиле выглядит как composable: ref становится state, computed — getter, функция — action. Этот стиль гибче (можно использовать другие composables внутри) и многим ближе.
<script>
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const count = computed(() => items.value.length)
function add(product) { items.value.push(product) }
return { items, count, add }
})
</script>
Использование в компоненте
В компоненте просто зовём хук store. Важная тонкость: чтобы достать реактивные поля через деструктуризацию, оборачивайте store в storeToRefs — иначе реактивность потеряется (та же история, что с reactive). Actions при этом можно деструктурировать напрямую, они не теряют связь.
<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from './stores/cart'
const cart = useCartStore()
const { items, count } = storeToRefs(cart) // реактивные state/getters
const { add, clear } = cart // actions — как есть
</script>
<template>
<p>Товаров: {{ count }}</p>
<button @click="clear">Очистить</button>
</template>
Использование вне компонентов
Store можно дёргать и из обычного JS-модуля — например, в роутере или в утилите. Единственное условие: Pinia уже должна быть установлена в приложение (app.use(pinia)) к моменту вызова. Внутри гварда роутера это всегда так, поэтому вызов безопасен:
// упрощённая модель «глобального синглтона», как у store вне компонента
function createStore() {
const state = { user: null }
return {
login: (name) => { state.user = name },
get user() { return state.user },
}
}
const auth = createStore()
function routeGuard(to) {
if (to === '/admin' && !auth.user) return '/login'
return to
}
console.log(routeGuard('/admin'))
auth.login('Аня')
console.log(routeGuard('/admin'))
Вывод:
/login /admin
Плагины Pinia
Плагин — функция, которую Pinia вызывает для каждого store. Через неё можно добавить общее свойство всем store или подписаться на изменения. Классический пример — автосохранение state в localStorage:
// плагин: сохраняем state каждого store при изменении
function persistPlugin({ store }) {
const saved = localStorage.getItem(store.$id)
if (saved) store.$patch(JSON.parse(saved))
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
// подключение: pinia.use(persistPlugin)
console.log('Плагин вызовется для каждого store по его $id')
Вывод:
Плагин вызовется для каждого store по его $id
Здесь видны служебные API store: $id — его идентификатор, $patch — частичное обновление state, $subscribe — подписка на любые мутации. На них и держатся плагины.
Pinia против Vuex
| Аспект | Vuex | Pinia |
| Мутации | отдельная сущность mutations | нет — меняем state прямо в actions |
| Модули | вложенные namespaced-модули | много плоских store |
| Типизация TS | громоздкая | выводится автоматически |
| API | больше шаблонного кода | минимализм, как Composition API |
Ключевое отличие: в Vuex для изменения state нужно было идти через mutations (а асинхронность — через actions, вызывающие mutations). Pinia убрала этот слой: state меняется напрямую в actions. Pinia — официально рекомендованный преемник Vuex.
Как это работает под капотом
Внутри Pinia state каждого store — это reactive-объект, getters — computed, а сам store кешируется как синглтон по своему id. Первый вызов useCartStore() создаёт и регистрирует store в активном экземпляре Pinia; последующие вызовы возвращают тот же объект — отсюда «глобальность». storeToRefs проходит по полям store и оборачивает state/getters в ref, оставляя методы нетронутыми. Плагины — это просто массив функций, который Pinia прогоняет при создании каждого store.
Частые ошибки
- Деструктурировать store напрямую (
const { items } = cart) — теряется реактивность; нуженstoreToRefs. - Прогонять
storeToRefsпо actions — лишнее; методы берут прямо со store. - Делать
stateобъектом, а не функцией — все приложения станут делить один объект (опасно в SSR). - Вызывать store до
app.use(pinia)— ошибка «no active Pinia». - Искать mutations как в Vuex — их нет, меняйте state прямо в action.
Итоги
- Store определяют через
defineStoreв options- или setup-стиле; id уникален. - State — функция с данными, getters — кешируемые
computed, actions — методы (могут быть async). - Деструктуризацию state/getters делайте через
storeToRefs, actions берите напрямую. - Store работает и вне компонентов, если Pinia уже установлена в приложение.
- Плагины расширяют все store через
$id,$patch,$subscribe; Pinia — преемник Vuex без mutations.