15. Pinia: управление состоянием
🍍 Pinia — управление состоянием
Заголовок раздела «🍍 Pinia — управление состоянием»Pinia — официальный state manager для Vue 3. Это как Vuex, но в 10 раз проще, с отличным TypeScript и потрясающими devtools. Автор Vue — Эван Ю — официально рекомендует Pinia вместо Vuex для новых проектов 🚀
Установка
Заголовок раздела «Установка»npm install piniaimport { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'
const pinia = createPinia()
createApp(App) .use(pinia) .mount('#app')defineStore — создание стора
Заголовок раздела «defineStore — создание стора»Pinia предлагает два синтаксиса: Options Store (как Vuex/Options API) и Setup Store (как Composition API). Оба полноценные, выбирай по вкусу!
Options Store — знакомый синтаксис
Заголовок раздела «Options Store — знакомый синтаксис»import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', { // state — реактивные данные (как data() в компоненте) state: () => ({ count: 0, name: 'Яша', }),
// getters — вычисляемые свойства (как computed) getters: { doubleCount: (state) => state.count * 2,
// Getter использующий другой getter doubleCountPlusTen(): number { return this.doubleCount + 10 },
// Getter с аргументом (возвращает функцию) countPlusFive: (state) => (multiplier: number) => { return state.count * multiplier }, },
// actions — методы (как methods, могут быть async) actions: { increment() { this.count++ },
decrement() { this.count = Math.max(0, this.count - 1) },
async fetchCount() { const response = await fetch('/api/count') const data = await response.json() this.count = data.count // Прямая мутация в action! }, },})Setup Store — Composition API стиль
Заголовок раздела «Setup Store — Composition API стиль»import { defineStore } from 'pinia'import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => { // state → ref/reactive const count = ref(0) const name = ref('Яша')
// getters → computed const doubleCount = computed(() => count.value * 2) const isNegative = computed(() => count.value < 0)
// actions → functions function increment() { count.value++ }
function decrement() { count.value-- }
async function fetchCount() { const response = await fetch('/api/count') const data = await response.json() count.value = data.count }
// Обязательно возвращаем всё! return { count, name, doubleCount, isNegative, increment, decrement, fetchCount, }})Использование в компонентах
Заголовок раздела «Использование в компонентах»<script setup lang="ts">import { storeToRefs } from 'pinia'import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// ❌ НЕЛЬЗЯ деструктурировать напрямую — потеряешь реактивность!const { count, name } = store // count и name НЕ реактивны!
// ✅ Используй storeToRefs для реактивных свойствconst { count, name, doubleCount } = storeToRefs(store)
// Методы и actions можно деструктурировать напрямую (они функции)const { increment, decrement, fetchCount } = store</script>
<template> <div> <p>{{ count }} (двойной: {{ doubleCount }})</p> <button @click="increment">+</button> <button @click="decrement">-</button>
<!-- Можно вызывать и через store --> <button @click="store.increment()">+</button> </div></template>$patch — групповое обновление состояния
Заголовок раздела «$patch — групповое обновление состояния»<script setup lang="ts">import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// Обновление одного свойстваuserStore.name = 'Катя' // Прямая мутация — работает!
// $patch — несколько свойств сразуuserStore.$patch({ name: 'Катя', age: 25,})
// $patch с функцией — для сложных мутацийuserStore.$patch((state) => { state.items.push({ id: 4, name: 'Новый' }) state.hasChanges = true // Всё это — одна транзакция в devtools!})</script>$reset — сброс состояния
Заголовок раздела «$reset — сброс состояния»<script setup lang="ts">import { useFormStore } from '@/stores/form'
const formStore = useFormStore()
// Сброс к начальным значениямfunction handleCancel() { formStore.$reset()}</script>⚠️
$reset()работает только для Options Store. В Setup Store нужно реализовать вручную или использовать плагин.
$subscribe — подписка на изменения
Заголовок раздела «$subscribe — подписка на изменения»<script setup lang="ts">import { onUnmounted } from 'vue'import { useCartStore } from '@/stores/cart'
const cartStore = useCartStore()
// Подписываемся на все изменения стораconst unsubscribe = cartStore.$subscribe((mutation, state) => { // mutation.type: 'direct' | 'patch object' | 'patch function' console.log('Тип мутации:', mutation.type) console.log('Новое состояние:', state)
// Автосохранение в localStorage! localStorage.setItem('cart', JSON.stringify(state))})
// Отписываемся при уничтожении компонентаonUnmounted(unsubscribe)</script>$onAction — подписка на actions
Заголовок раздела «$onAction — подписка на actions»// Логирование всех actionsstore.$onAction(({ name, // Имя action store, // Экземпляр стора args, // Аргументы action after, // Хук после успешного выполнения onError, // Хук при ошибке}) => { const start = Date.now() console.log(\`Action "\${name}" начат с аргументами:\`, args)
after((result) => { console.log(\`Action "\${name}" завершён за \${Date.now() - start}мс\`) console.log('Результат:', result) })
onError((error) => { console.error(\`Action "\${name}" упал с ошибкой:\`, error) })})Сложный пример: Store для аутентификации
Заголовок раздела «Сложный пример: Store для аутентификации»import { defineStore } from 'pinia'import { ref, computed } from 'vue'
interface User { id: number name: string email: string role: 'admin' | 'user' avatar: string}
export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const token = ref<string | null>(localStorage.getItem('token')) const loading = ref(false) const error = ref<string | null>(null)
const isLoggedIn = computed(() => !!user.value && !!token.value) const isAdmin = computed(() => user.value?.role === 'admin') const displayName = computed(() => user.value?.name ?? 'Гость')
async function login(email: string, password: string) { loading.value = true error.value = null
try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), })
if (!response.ok) { throw new Error('Неверный email или пароль') }
const data = await response.json() token.value = data.token user.value = data.user
// Сохраняем токен localStorage.setItem('token', data.token) } catch (e) { error.value = e instanceof Error ? e.message : 'Ошибка входа' throw e // Пробрасываем для обработки в компоненте } finally { loading.value = false } }
async function logout() { user.value = null token.value = null localStorage.removeItem('token') }
async function fetchMe() { if (!token.value) return
try { const response = await fetch('/api/me', { headers: { Authorization: \`Bearer \${token.value}\` } }) user.value = await response.json() } catch { await logout() } }
return { user, token, loading, error, isLoggedIn, isAdmin, displayName, login, logout, fetchMe, }})Pinia vs Vuex — сравнение
Заголовок раздела «Pinia vs Vuex — сравнение»┌─────────────────┬──────────────────┬──────────────────┐│ │ Vuex 4 │ Pinia │├─────────────────┼──────────────────┼──────────────────┤│ Синтаксис │ Сложный │ Простой ││ TypeScript │ 😢 Плохо │ ✅ Отлично ││ Mutations │ Обязательны │ ❌ Не нужны ││ Модули │ Сложные │ Просто сторы ││ Devtools │ ✅ │ ✅ Лучше ││ Размер │ ~10kb │ ~2kb ││ SSR │ ✅ │ ✅ ││ Composables │ ❌ │ ✅ Setup Store │└─────────────────┴──────────────────┴──────────────────┘