Настройка State Management (Pinia) для Vue-приложения
Pinia — официальный state manager для Vue 3. Заменила Vuex начиная с Vue 3.x: проще API, полная поддержка TypeScript без дополнительных настроек, работает с Composition API, поддерживает devtools из коробки.
Стор в Pinia — это defineStore, который возвращает хук. Никаких мутаций, никаких неймспейсов, никаких модулей — просто функция с реактивным состоянием.
Что входит в работу
Установка и настройка Pinia, создание сторов под задачи проекта, типизация, async-действия, персистентность, интеграция с Vue Router и Axios, devtools.
Установка
npm install pinia
Регистрация в приложении:
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
Options Store
Синтаксис, похожий на Vuex Options API:
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
loading: false,
error: null as string | null,
}),
getters: {
total: (state) =>
state.items.reduce((sum, i) => sum + i.price * i.quantity, 0),
count: (state) =>
state.items.reduce((sum, i) => sum + i.quantity, 0),
isEmpty: (state) => state.items.length === 0,
},
actions: {
addItem(product: Product) {
const existing = this.items.find((i) => i.id === product.id)
if (existing) {
existing.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
},
removeItem(id: string) {
this.items = this.items.filter((i) => i.id !== id)
},
async checkout() {
this.loading = true
this.error = null
try {
await api.post('/orders', { items: this.items })
this.$reset()
} catch (err) {
this.error = err instanceof Error ? err.message : 'Ошибка'
} finally {
this.loading = false
}
},
},
})
Setup Store (Composition API)
Более гибкий вариант — ближе к Composition API:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const isAuthenticated = computed(() => token.value !== null)
const roles = computed(() => user.value?.roles ?? [])
function hasRole(role: string) {
return roles.value.includes(role)
}
async function login(credentials: LoginCredentials) {
const { data } = await axios.post<LoginResponse>('/api/auth/login', credentials)
user.value = data.user
token.value = data.token
axios.defaults.headers.common['Authorization'] = `Bearer ${data.token}`
}
function logout() {
user.value = null
token.value = null
delete axios.defaults.headers.common['Authorization']
}
return { user, token, isAuthenticated, roles, hasRole, login, logout }
})
Использование в компонентах
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cart = useCartStore()
// storeToRefs сохраняет реактивность при деструктуризации
const { items, total, count, loading } = storeToRefs(cart)
// actions деструктурируем напрямую (они не теряют контекст)
const { addItem, removeItem, checkout } = cart
</script>
<template>
<div>
<p>Товаров: {{ count }}, Сумма: {{ total }} ₽</p>
<button @click="checkout" :disabled="loading || cart.isEmpty">
{{ loading ? 'Оформление...' : 'Оформить заказ' }}
</button>
</div>
</template>
Деструктуризация напрямую (const { items } = cart) потеряет реактивность — нужен storeToRefs.
Взаимодействие между сторами
export const useOrderStore = defineStore('orders', () => {
const orders = ref<Order[]>([])
async function createOrder() {
// используем другой стор внутри
const cart = useCartStore()
const auth = useAuthStore()
if (!auth.isAuthenticated) throw new Error('Необходима авторизация')
const order = await api.post<Order>('/orders', {
items: cart.items,
userId: auth.user!.id,
})
orders.value.push(order)
cart.$reset()
return order
}
return { orders, createOrder }
})
Персистентность
npm install pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useAuthStore = defineStore('auth', {
state: () => ({ user: null, token: null }),
persist: {
key: 'auth',
storage: localStorage,
paths: ['token'], // сохраняем только token
},
})
Тестирование
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '@/stores/cart'
beforeEach(() => {
setActivePinia(createPinia())
})
test('addItem увеличивает количество', () => {
const cart = useCartStore()
cart.addItem({ id: '1', name: 'Test', price: 200 })
cart.addItem({ id: '1', name: 'Test', price: 200 })
expect(cart.count).toBe(2)
expect(cart.total).toBe(400)
})
test('checkout вызывает api и сбрасывает корзину', async () => {
vi.spyOn(api, 'post').mockResolvedValue({})
const cart = useCartStore()
cart.addItem({ id: '1', name: 'Test', price: 100 })
await cart.checkout()
expect(cart.isEmpty).toBe(true)
})
Структура файлов
src/
stores/
auth.store.ts
cart.store.ts
ui.store.ts
orders.store.ts
index.ts # реэкспорт
Что делаем
Устанавливаем и настраиваем Pinia, проектируем сторы под предметную область, подключаем персистентность для нужных данных, настраиваем интеграцию с axios (токен в headers), покрываем тестами с помощью Vitest.
Срок: 1–2 дня.







