Руководство для начинающих по Pinia
В Vue3 управление состоянием — тема, которую невозможно обойти.
Pinia — это лёгкая библиотека управления состоянием для Vue.js, позволяющая совместно использовать и управлять состоянием между компонентами. Pinia можно представить как глобальное хранилище данных, из которого все компоненты могут получать или обновлять данные.
В этой главе мы познакомимся с Pinia — официально рекомендованной библиотекой управления состоянием для Vue3. По сравнению с Vuex, Pinia предлагает более простое решение для управления состоянием, которое лучше соответствует философии Composition API во Vue3.
На следующей схеме сравниваются структурные различия между ними:
- Vuex использует единое хранилище с многоуровневыми модулями в древовидной структуре, с фиксированной иерархией, полагается на мутации и в целом более тяжёлый.
- Pinia, напротив, состоит из нескольких независимых хранилищ; она плоская, лёгкая, не имеет разделения на модули и накладных расходов на пространства имён.

Ключевые возможности Pinia:
- Поддерживает Vue2 и Vue3
- Минималистичный дизайн API
- Полная поддержка TypeScript
- Поддерживает Composition API
- Модульный дизайн без вложенных модулей
Плоская структура Pinia позволяет компонентам напрямую подключаться к любому хранилищу и брать то, что им нужно, без прохождения через древовидные модули или пути пространств имён. Поток состояния простой и прямой, с низкой связанностью.

Установка и настройка
Создайте проект с помощью Vite:
npm create vite@latest vue-pinia-demo --template vue cd vue-pinia-demo npm install
Установите Pinia
Сначала установите Pinia в вашем проекте на Vue3:
npm install pinia # 或者 yarn add pinia
Pinia состоит из трёх частей:
- state: хранит данные
- getters: вычисляют производные данные
- actions: выполняют логику, изменяют состояние
Настройте Pinia
Настройте Pinia в src/main.js или src/main.ts:
Пример
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// Создайте экземпляр Pinia
const pinia = createPinia()
// Создайте приложение Vue
const app = createApp(App)
// Используйте Pinia
app.use(pinia)
app.mount('#app')
Создайте свой первый Store
Store — это хранилище данных в Pinia. Давайте создадим простое хранилище счётчика.
Определите Store
Создайте новый файл src/stores/useCounter.js:
Пример
import { defineStore } from 'pinia'
// Определите хранилище с помощью defineStore
// Первый параметр — уникальный идентификатор хранилища
// Второй параметр — параметры конфигурации хранилища
export const useCounterStore = defineStore('counter', {
// state: определяет данные состояния хранилища
state: () => ({
count: 0,
name: 'My Counter'
}),
// getters: определяет вычисляемые свойства на основе state
геттеры: {
doubleCount: (state) => state.count * 2,
// Используйте this для доступа к другим геттерам
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
// actions: определяет методы для изменения состояния
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
// Может принимать параметры
incrementBy(amount) {
this.count += amount
},
// Асинхронное действие
async incrementAsync() {
// Имитация асинхронной операции
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
})
Объяснение структуры хранилища
Давайте разберём различные части хранилища с помощью таблицы:
| Часть | Назначение | Пример |
|---|---|---|
| state | Определяет хранимые данные | count: 0 |
| геттеры | Вычисляемые свойства на основе состояния | doubleCount: state => state.count * 2 |
| actions | Методы изменения состояния | increment() { this.count++ } |
Использование хранилища в компонентах
Базовое использование
Код компонента для src/components/CounterComponent.vue выглядит так:
Пример
<template>
<div class="counter">
<h3>{{ store.name }}</h3>
<p>Текущее значение: {{ store.count }}</p>
<p>Удвоенное значение: {{ store.doubleCount }}</p>
<p>Удвоенное значение плюс один: {{ store.doubleCountPlusOne }}</p>
<button @click="store.increment()">+1</button>
<button @click="store.decrement()">-1</button>
<button @click="store.incrementBy(5)">+5</button>
<button @click="store.incrementAsync()">Асинхронно +1</button>
<button @click="reset">Сбросить</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/useCounter'
// Использовать store в setup
const store = useCounterStore()
// Метод для сброса состояния
function reset() {
store.$reset() // Метод $reset может сбросить состояние до его начального значения
}
</script>
Измените src/App.vue следующим образом:
Пример
import CounterComponent from './components/CounterComponent.vue'
</script>
<template>
<CounterComponent />
</template>
Вся структура проекта:

Запуститеnpm run devкоманду и откройте в браузереhttp://localhost:5173/, чтобы увидеть результат:

Реактивная деструктуризация
Если вы хотите напрямую использовать свойства состояния в шаблоне, вы можете использоватьstoreToRefs:
Пример
<div>
<p>Количество: {{ count }}</p>
<p>Имя: {{ name }}</p>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// Используйте storeToRefs, чтобы сохранить реактивность
const { count, name } = storeToRefs(store)
// Примечание: деструктуризация напрямую приведёт к потере реактивности!
// Неправильный способ: const { count, name } = store
</script>
Хранилище в стиле Composition API
Pinia также поддерживает определение хранилищ в стиле Composition API:
Пример
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// состояние
const user = ref(null)
const isLoggedIn = ref(false)
// геттеры
const userName = computed(() => user.value?.name || 'Гость')
const userAge = computed(() => user.value?.age || 0)
// действия
function login(userData) {
user.value = userData
isLoggedIn.value = true
}
function logout() {
user.value = null
isLoggedIn.value = false
}
return {
user,
isLoggedIn,
userName,
userAge,
login,
logout
}
})
Взаимодействие между хранилищами
Несколько хранилищ могут вызывать друг друга:
Пример
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
addItem(product) {
this.items.push(product)
// Вызвать действие из другого хранилища
const userStore = useUserStore()
if (userStore.isLoggedIn) {
// Синхронизировать с корзиной покупок пользователя
this.syncToUserCart()
}
}
}
})
Практический пример: корзина покупок
Давайте создадим полный пример корзины покупок:
Пример
export const useProductStore = defineStore('products', {
state: () => ({
products: [
{ id: 1, name: 'Laptop', price: 5999, stock: 10 },
{ id: 2, name: 'Смартфон', price: 3999, stock: 20 },
{ id: 3, name: 'Беспроводные наушники', price: 299, stock: 50 }
]
}),
геттеры: {
// Получить товар по ID
getProductById: (state) => (id) => {
return state.products.find(product => product.id === id)
},
// Получить товары в наличии
availableProducts: (state) => {
return state.products.filter(product => product.stock > 0)
}
}
})
Пример
export const useCartStore = defineStore('cart', {
state: () => ({
items: [], // { productId, quantity }
discount: 0
}),
геттеры: {
// Общее количество товаров в корзине
totalItems: (state) => {
return state.items.reduce((total, item) => total + item.quantity, 0)
},
// Общая сумма корзины
totalPrice: (state) => {
const productStore = useProductStore()
return state.items.reduce((total, item) => {
const product = productStore.getProductById(item.productId)
return total + (product?.price || 0) * item.quantity
}, 0)
},
// Итоговая цена после скидки
finalPrice: (state) => {
return state.totalPrice * (1 - state.discount / 100)
}
},
actions: {
// Добавить товар в корзину
addToCart(productId, quantity = 1) {
const existingItem = this.items.find(item => item.productId === productId)
if (existingItem) {
existingItem.quantity += quantity
} else {
this.items.push({ productId, quantity })
}
},
// Удалить товар из корзины
removeFromCart(productId) {
this.items = this.items.filter(item => item.productId !== productId)
},
// Очистить корзину
clearCart() {
this.items = []
this.discount = 0
},
// Установить скидку
setDiscount(percent) {
this.discount = Math.max(0, Math.min(100, percent))
}
}
})
Использование корзины покупок в компоненте
Пример
<template>
<div class="shopping-cart">
<h3>Корзина покупок ({{ cart.totalItems }} товаров)</h3>
<div v-if="cart.items.length === 0">
<p>Корзина пуста</p>
</div>
<div v-else>
<div v-for="item in cartItems" :key="item.product.id" class="cart-item">
<span>{{ item.product.name }}</span>
<span>¥{{ item.product.price }}</span>
<span>Количество: {{ item.quantity }}</span>
<span>Подытог: ¥{{ item.product.price * item.quantity }}</span>
<button @click="cart.removeFromCart(item.product.id)">Удалить</button>
</div>
<div class="cart-summary">
<p>Итого: ¥{{ cart.totalPrice }}</p>
<p v-if="cart.discount >0">Скидка: {{ cart.discount }}%</p>
<p>Оплачено: ¥{{ cart.finalPrice }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/products'
const cart = useCartStore()
const products = useProductStore()
// Вычислить сведения о товарах в корзине
const cartItems = computed(() => {
return cart.items.map(item => ({
product: products.getProductById(item.productId),
quantity: item.quantity
})).filter(item => item.product) // Исключить несуществующие товары
})
</script>
Лучшие практики и рекомендации
1. Соглашения об именовании хранилищ
Пример
export const useUserStore = defineStore('user', { /* ... */ })
export const useProductStore = defineStore('products', { /* ... */ })
// Именование, которого следует избегать
export const userStore = defineStore('user', { /* ... */ }) // Отсутствует префикс use
2. Инициализация состояния
Пример
state: () => ({
items: [],
loading: false,
error: null
})
// Не рекомендуется: прямое использование объекта
state: {
items: [] // Это приведёт к тому, что все экземпляры будут использовать один и тот же массив!
}
3. Обработка асинхронных операций
Пример
async fetchUserData(userId) {
this.loading = true
this.error = null
try {
const response = await api.getUser(userId)
this.user = response.data
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
}
}
4. Сохранение данных
Для данных, которые нужно сохранять, можно использовать плагин:
npm install pinia-plugin-persistedstate
Пример
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', {
state: () => ({
токен: '',
userInfo: {}
}),
// Включить сохранение
persist: true
})
Pinia API
| Категория | API | Описание |
|---|---|---|
| Создание | defineStore |
Определите независимое хранилище, содержащее state / getters / actions |
| Создание экземпляра | createPinia |
Создайте корневой экземпляр Pinia для app.use() |
| Использование в компоненте | useStore()(Определяемый пользователем, например,useUserStore) |
Вызов хранилища, возвращает реактивное состояние, геттеры и действия |
| State | state |
Возвращает объект-функцию, используется для определения изменяемого глобального состояния |
| Операции с состоянием | store.$state |
Прямое чтение/запись всего состояния (на уровне объекта) |
| Операции с состоянием | store.$patch() |
Пакетное изменение состояния, поддерживает как объектный, так и функциональный режимы |
| Замена состояния | store.$reset() |
Сброс к исходному состоянию, доступен только при определении через метод setup |
| Подписка на состояние | store.$subscribe() |
Прослушивание изменений состояния, подходит для локального сохранения |
| Вызов действий | store.$onAction() |
Прослушивание до и после вызовов действий, можно использовать для логирования и отслеживания |
| Геттер | getters |
Производные данные, автоматически кешируемые на основе state |
| Плагины | pinia.use() |
Регистрация плагинов для расширения возможностей хранилища |
| Метод определения | defineStore(id, options) |
Синтаксис Options API |
| Метод определения | defineStore(id, () => {...}) |
Синтаксис setup, возвращает state / getter / action |
| Свойства хранилища | store.$id |
Уникальный идентификатор текущего хранилища |
| Свойства хранилища | store.$ready(в некоторых версиях) |
Флаг состояния после завершения инициализации хранилища |
| Персистентность (плагин) | persist |
Включение плагинов хранилища (например, localStorage) |
| Вспомогательные функции | storeToRefs() |
Преобразование state / getters в refs с сохранением реактивности |
Другие расширенияДля получения дополнительной информации обратитесь к официальному сайту Pinia:https://pinia.vuejs.org/