Зачем нужен Pinia и как устроен store?
Вопрос
Зачем нужно глобальное управление состоянием, как устроен store в Pinia и чем это отличается от Vuex?
Короткий ответ
Глобальное управление состоянием нужно, когда данные используются во многих несвязанных компонентах и передавать их через props/emits на несколько уровней вложенности («prop drilling») становится неудобно. Pinia — официальная библиотека состояния для Vue 3, store в ней состоит из state (данные), getters (производные значения, аналог computed) и actions (методы, включая асинхронные, для изменения state). По сравнению с Vuex, Pinia проще, не требует мутаций и модулей с пространствами имён, полностью поддерживает TypeScript «из коробки».
Подробный ответ
Зачем нужен store. Когда два компонента, далёкие друг от друга в дереве, должны читать и менять одни и те же данные (корзина покупок, данные авторизованного пользователя, тема оформления), передавать это через цепочку props вниз и events вверх становится громоздко. Store — это единый источник истины (single source of truth), реактивный объект вне дерева компонентов, к которому можно обратиться из любого места напрямую.
Структура store в Pinia (Composition-style синтаксис через defineStore):
state— функция, возвращающая объект с начальными данными (аналогdata()у компонента). Возвращаемые значения автоматически становятся реактивными.getters— производные, кэшируемые значения от state, концептуально эквивалентныcomputed. Могут обращаться кthis(в options-синтаксисе) или получатьstateпараметром.actions— методы для изменения state, включая асинхронную логику (запросы к API). В Pinia, в отличие от Vuex, нет разделения на mutations и actions — state можно менять прямо внутри actions.
Pinia поддерживает и Setup Store синтаксис — store пишется как обычная composition-функция: ref становится state, computed — getters, обычные функции — actions. Это даёт максимальную гибкость и единообразие с остальным кодом на Composition API.
Отличия от Vuex:
| Vuex | Pinia | |
|---|---|---|
| Изменение состояния | через mutations (обязательно) + actions | напрямую внутри actions |
| Модули | вложенные, с пространствами имён (namespaced: true) | плоские отдельные stores, импортируются напрямую |
| TypeScript | требует дополнительной обвязки для типизации | типизация выводится автоматически |
| Devtools | поддерживается | поддерживается, включая time-travel debugging |
| API | this.$store.commit/dispatch | вызов методов store напрямую, как обычных функций |
Пример
// stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] // [{ id, price, quantity }]
}),
getters: {
totalPrice: (state) =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
itemCount: (state) => state.items.length
},
actions: {
addItem(product) {
const existing = this.items.find((i) => i.id === product.id)
if (existing) {
existing.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
},
async checkout() {
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify(this.items)
})
this.items = []
}
}
})
<!-- CartSummary.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
<p>Товаров в корзине: {{ cart.itemCount }}</p>
<p>Итого: {{ cart.totalPrice }} ₽</p>
<button @click="cart.checkout()">Оформить заказ</button>
</template>
Дополнительные вопросы
- Чем Setup Store в Pinia отличается от Options Store и когда какой выбрать?
- Как сделать состояние store персистентным (сохранить в localStorage между перезагрузками)?
- Почему Vuex требовал mutations для изменения state, а Pinia — нет?