Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Pinia: управление состоянием
pinia

Pinia: управление состоянием

defineStore, state, getters, actions, storeToRefs, плагин персистентности и тестирование сторов

Pinia: управление состоянием

Pinia — официальный стейт-менеджер для Vue 3. Он проще Vuex, полностью типобезопасен и спроектирован специально под Composition API. Pinia рекомендуется командой Vue как замена Vuex.

#Зачем нужен стейт-менеджмент

В небольших приложениях данные передаются через props вниз и emits вверх. Но когда дерево компонентов разрастается, возникает «prop drilling» — данные приходится передавать через 3-5 уровней промежуточных компонентов, которые их даже не используют.

provide/inject частично решает эту проблему, но не даёт централизованного управления состоянием, DevTools-поддержки и возможности отлаживать изменения данных.

Pinia решает эти задачи: глобальное хранилище с реактивным состоянием, доступное из любого компонента, с полной поддержкой Vue DevTools и TypeScript.

#Установка Pinia

Устанавливаем Pinia и регистрируем в приложении:

npm install pinia
// src/main.js import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.mount('#app')

#defineStore — два стиля определения

Pinia поддерживает два способа определения стора: Options API стиль (похож на Vuex) и Setup API стиль (похож на <script setup>).

Options API стиль — знаком тем, кто работал с Vuex:

// stores/counter.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { // state — начальное состояние (функция, чтобы работало в SSR) state: () => ({ count: 0, name: 'Счётчик' }), // getters — вычисляемые значения на основе state getters: { doubled: (state) => state.count * 2, // Геттер может использовать другие геттеры через this doubledPlusOne(): number { return this.doubled + 1 } }, // actions — синхронные и асинхронные методы actions: { increment() { this.count++ }, setName(name) { this.name = name }, async fetchInitialCount() { const response = await fetch('/api/counter') const data = await response.json() this.count = data.count } } })

Setup API стиль — более гибкий, ближе к Composition API:

// stores/user.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useUserStore = defineStore('user', () => { // ref() → state const user = ref(null) const isLoading = ref(false) const error = ref(null) // computed() → getters const isLoggedIn = computed(() => !!user.value) const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '' ) // function → actions async function login(credentials) { isLoading.value = true error.value = null try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }) if (!response.ok) throw new Error('Неверные данные') user.value = await response.json() } catch (e) { error.value = e.message } finally { isLoading.value = false } } function logout() { user.value = null } // Обязательно возвращаем всё, что нужно компонентам return { user, isLoading, error, isLoggedIn, fullName, login, logout } })

Setup стиль предпочтителен для сложных сторов — он даёт полный доступ к Composition API: watch, watchEffect, composables.

#Использование стора в компонентах

Стор подключается в компоненте через соответствующий хук. Важный момент: нельзя деструктурировать стор напрямую — потеряется реактивность:

<template> <div> <!-- Используем свойства стора --> <p>Счётчик: {{ counter.count }} (удвоено: {{ counter.doubled }})</p> <button @click="counter.increment()">+1</button> <!-- Из user store --> <div v-if="userStore.isLoggedIn"> <p>Привет, {{ userStore.fullName }}!</p> <button @click="userStore.logout()">Выйти</button> </div> </div> </template> <script setup> import { useCounterStore } from '@/stores/counter' import { useUserStore } from '@/stores/user' const counter = useCounterStore() const userStore = useUserStore() // НЕПРАВИЛЬНО — деструктуризация ломает реактивность: // const { count, increment } = useCounterStore() // count не реактивен! </script>

#storeToRefs — безопасная деструктуризация

storeToRefs позволяет деструктурировать state и getters стора, сохраняя реактивность. Actions можно деструктурировать напрямую (они не реактивны):

<script setup> import { storeToRefs } from 'pinia' import { useCounterStore } from '@/stores/counter' import { useUserStore } from '@/stores/user' const counterStore = useCounterStore() const userStore = useUserStore() // storeToRefs для state и getters — сохраняет реактивность const { count, doubled, name } = storeToRefs(counterStore) const { user, isLoggedIn, fullName, isLoading } = storeToRefs(userStore) // Actions деструктурируем напрямую — они не реактивны const { increment, setName } = counterStore const { login, logout } = userStore </script> <template> <!-- Теперь можно использовать напрямую --> <p>{{ count }} ({{ doubled }})</p> <button @click="increment">+1</button> <p v-if="isLoggedIn">{{ fullName }}</p> </template>

#Взаимодействие между сторами

Сторы могут использовать друг друга. В Setup стиле это делается через обычный импорт:

// stores/cart.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useUserStore } from './user' export const useCartStore = defineStore('cart', () => { const items = ref([]) const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) ) const itemCount = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) ) function addItem(product) { // Используем другой стор const userStore = useUserStore() if (!userStore.isLoggedIn) { throw new Error('Необходима авторизация') } const existing = items.value.find(i => i.id === product.id) if (existing) { existing.quantity++ } else { items.value.push({ ...product, quantity: 1 }) } } function removeItem(productId) { items.value = items.value.filter(i => i.id !== productId) } async function checkout() { const userStore = useUserStore() const response = await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: userStore.user.id, items: items.value }) }) if (response.ok) { items.value = [] } } return { items, total, itemCount, addItem, removeItem, checkout } })

#$patch — обновление нескольких полей

Pinia предоставляет метод $patch для атомарного обновления нескольких полей состояния:

<script setup> import { useCounterStore } from '@/stores/counter' const store = useCounterStore() // Обновление объектом store.$patch({ count: 10, name: 'Новое имя' }) // Обновление через функцию — удобно для массивов и сложной логики store.$patch((state) => { state.items.push({ id: 3, name: 'Новый элемент' }) state.count++ }) // Полный сброс к начальному состоянию store.$reset() </script>

#Подписка на изменения

Pinia позволяет подписываться на изменения state и actions:

import { useCounterStore } from '@/stores/counter' const store = useCounterStore() // Подписка на изменения state const unsubscribe = store.$subscribe((mutation, state) => { console.log('State изменился:', mutation.type, state) // Сохраняем в localStorage при каждом изменении localStorage.setItem('counter', JSON.stringify(state)) }) // Подписка на actions store.$onAction(({ name, store, args, after, onError }) => { console.log(`Action "${name}" вызвана с аргументами:`, args) after((result) => { console.log(`Action "${name}" завершилась:`, result) }) onError((error) => { console.error(`Action "${name}" завершилась с ошибкой:`, error) }) }) // Отписка при уничтожении компонента onUnmounted(() => unsubscribe())

#Persistence с pinia-plugin-persistedstate

Плагин pinia-plugin-persistedstate автоматически сохраняет и восстанавливает состояние из localStorage или sessionStorage:

npm install pinia-plugin-persistedstate
// main.js import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate)
// stores/auth.js export const useAuthStore = defineStore('auth', { state: () => ({ user: null, accessToken: null }), // persist: true — сохраняет всё состояние persist: { // Ключ в localStorage key: 'auth', // Только эти поля pick: ['accessToken'], // Или использовать sessionStorage storage: sessionStorage } })

#Тестирование сторов

Pinia легко тестируется — достаточно создать тестовое окружение с createPinia():

// stores/counter.test.js import { setActivePinia, createPinia } from 'pinia' import { useCounterStore } from '@/stores/counter' import { describe, it, expect, beforeEach } from 'vitest' describe('Counter Store', () => { beforeEach(() => { // Создаём свежий pinia для каждого теста setActivePinia(createPinia()) }) it('начальное значение count = 0', () => { const store = useCounterStore() expect(store.count).toBe(0) }) it('increment увеличивает count', () => { const store = useCounterStore() store.increment() store.increment() expect(store.count).toBe(2) }) it('doubled возвращает удвоенное значение', () => { const store = useCounterStore() store.count = 5 expect(store.doubled).toBe(10) }) it('можно замокать action', async () => { const store = useCounterStore() // Мокаем fetch для тестирования async action global.fetch = vi.fn().mockResolvedValue({ json: () => Promise.resolve({ count: 42 }) }) await store.fetchInitialCount() expect(store.count).toBe(42) }) })

Далее: Основы Nuxt 3