Vue.jsMiddleTechnical

Что такое Pinia? Чем она отличается от Vuex и почему является рекомендованным менеджером состояния для Vue 3?

Pinia — официальный стор для Vue 3, заменивший Vuex: нет mutations, нет namespaced-модулей, полный TypeScript inference, два стиля API (Options и Setup). Vuex 5 так и не вышел, поэтому Pinia стала рекомендованным решением.

Что такое Pinia

Pinia — официальный менеджер состояния Vue 3, пришедший на смену Vuex. Разработана членом команды Vue Эдуардо Сан-Мартином, включена в экосистему Vue как рекомендованное решение начиная с Vue 3.2+. Реализована поверх Composition API и Proxy-реактивности Vue 3.

Минимальный пример

// stores/counter.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  return { count, doubled, increment };
});
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
app.use(createPinia());
app.mount('#app');
// MyComponent.vue (script setup)
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';

const store = useCounterStore();
const { count, doubled } = storeToRefs(store); // реактивная деструктуризация

Отличия от Vuex

  • Нет mutations. В Vuex изменение состояния требовало двух шагов: action → mutation. В Pinia состояние меняется напрямую внутри action или через store.count++.
  • Нет namespaced-модулей. Каждый store — отдельный вызов defineStore() с уникальным id. Импорт явный, нет строковых путей вида 'auth/login'.
  • TypeScript из коробки. Типы выводятся автоматически без ручных деклараций, в отличие от Vuex 4, где требовались дополнительные typed wrappers.
  • Devtools. Pinia интегрируется с Vue Devtools: timeline actions, time-travel для каждого store, hot-module replacement состояния.
  • Два стиля API. Options Store (похож на компоненты с state, getters, actions) и Setup Store (обычная setup-функция с ref/computed). Оба поддерживаются официально.

Options Store (альтернативный синтаксис)

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [], total: 0 }),
  getters: {
    itemCount: (state) => state.items.length,
  },
  actions: {
    addItem(product) {
      this.items.push(product);
      this.total += product.price;
    },
    async fetchCart(userId) {
      const data = await fetch(`/api/cart/${userId}`).then(r => r.json());
      this.items = data.items;
    }
  }
});

Плагины Pinia

Pinia поддерживает плагины — функции, получающие контекст каждого store. Например, плагин персистентности:

import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

Затем в store добавляется persist: true в опции defineStore.

Почему Pinia рекомендована для Vue 3

  • Vuex 5 (изначально планировавшийся для Vue 3) так и не вышел — Pinia стала его заменой.
  • Нативно использует Composition API, что делает код stores идентичным composables по стилю.
  • SSR-поддержка (Nuxt 3) работает без дополнительной конфигурации.
  • Тестирование: setActivePinia(createPinia()) в beforeEach — store изолирован для каждого теста.

Подводные камни

  • Деструктуризация store напрямую (const { count } = useCounterStore()) разрушает реактивность — нужен storeToRefs() для state/getters и обычная деструктуризация для actions.
  • Вызов store вне компонента (например, в router guard) должен происходить после app.use(pinia), иначе — ошибка «no active pinia».
  • В SSR каждый запрос должен создавать новый экземпляр pinia — общий singleton приведёт к утечке состояния между пользователями.
  • Прямое изменение store.$state в продакшене допустимо, но в тестах лучше использовать store.$patch() для читаемости.
  • При использовании Setup Store нет автоматического reset — нужно реализовывать $reset() вручную через store.$patch(initialState).
  • Circular imports между stores (store A вызывает useStoreB внутри action, store B — useStoreA) создают проблемы при холодном старте — рефакторить через lazy import внутри action.
  • HMR работает только если добавить блок if (import.meta.hot) { import.meta.hot.accept(acceptHMRUpdate(useStore, import.meta.hot)) }.

Common mistakes

  • Перенести Vuex-привычку с mutations и писать в Pinia такой же boilerplate.
  • Деструктурировать стор без storeToRefs и терять реактивность.
  • Хранить во Pinia то, что должно быть локально в компоненте.
  • Прятать API-запросы в getters/computed вместо actions.

What the interviewer is testing

  • Знает базовый API defineStore + setup-style.
  • Может объяснить разницу с Vuex: нет mutations, лучше TS.
  • Понимает роль storeToRefs.
  • Помнит про SSR-гидрацию состояния.

Sources

Related topics