Плагины для Pinia

Плагин Pinia — это функция, которая встраивается в жизненный цикл хранилища и получает доступ к его внутреннему состоянию, действиям и метаданным. Плагины позволяют расширять поведение всех или отдельных store без дублирования кода и без изменения их исходной логики.

На уровне архитектуры плагин — это промежуточный слой между созданием store и его использованием в приложении. Он может:

  • добавлять новые свойства и методы в store;
  • отслеживать изменения состояния;
  • перехватывать вызовы actions;
  • синхронизировать состояние с внешними источниками;
  • внедрять сквозную функциональность (логирование, кэширование, метрики).

Подключение плагина

Плагины регистрируются на уровне экземпляра Pinia и автоматически применяются ко всем store.

import { createPinia } from 'pinia'

const pinia = createPinia()

pinia.use((context) => {
  // логика плагина
})

app.use(pinia)

Каждый плагин — это функция, принимающая контекст плагина, который содержит информацию о store и инструменты для взаимодействия с ним.


Контекст плагина

Контекст, передаваемый в плагин, имеет следующую структуру:

{
  pinia,   // экземпляр Pinia
  app,     // Vue-приложение
  store,   // текущий store
  options  // опции store
}

store

Ключевой объект, представляющий конкретное хранилище. Через него доступно:

  • store.$id — идентификатор store;
  • store.$state — реактивное состояние;
  • store.$patch() — атомарное изменение состояния;
  • store.$subscribe() — подписка на мутации;
  • actions и getters.

Добавление свойств и методов в store

Плагин может расширять store, добавляя новые свойства или функции. Это делается путём возврата объекта из плагина.

pinia.use(() => {
  return {
    createdAt: Date.now()
  }
})

Теперь каждый store будет иметь свойство createdAt.

Добавление методов:

pinia.use(() => ({
  reset() {
    this.$reset()
  }
}))

Метод reset становится частью API каждого store и работает в его контексте.


Условное применение плагина

Не всегда требуется применять плагин ко всем store. Условие можно задать на основе store.$id или пользовательских опций.

pinia.use(({ store }) => {
  if (store.$id !== 'user') return

  return {
    isUserStore: true
  }
})

Использование кастомных опций store:

export const useCartStore = defineStore('cart', {
  state: () => ({}),
  enableLogger: true
})
pinia.use(({ options, store }) => {
  if (!options.enableLogger) return

  store.$subscribe((mutation, state) => {
    console.log(mutation, state)
  })
})

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

Pinia предоставляет механизм подписки на мутации состояния внутри плагина.

pinia.use(({ store }) => {
  store.$subscribe((mutation, state) => {
    console.log(`[${store.$id}]`, mutation)
  })
})

Типы мутаций:

  • direct — прямое изменение состояния;
  • patch object — изменение через $patch({});
  • patch function — изменение через $patch(fn).

Подписка полезна для логирования, аналитики и синхронизации.


Перехват действий (actions)

Каждое действие можно обернуть, чтобы выполнить код до или после его вызова.

pinia.use(({ store }) => {
  const originalActions = store.$actions

  Object.keys(originalActions).forEach((actionName) => {
    const original = store[actionName]

    store[actionName] = async function (...args) {
      console.log('before', actionName)
      const result = await original.apply(this, args)
      console.log('after', actionName)
      return result
    }
  })
})

Этот подход применяется для:

  • глобального логирования;
  • обработки ошибок;
  • замеров времени выполнения;
  • автоматического отображения загрузки.

Плагин для сохранения состояния (persist)

Классический пример использования плагина — сохранение состояния в localStorage.

pinia.use(({ store }) => {
  const key = `pinia-${store.$id}`

  const saved = localStorage.getItem(key)
  if (saved) {
    store.$patch(JSON.parse(saved))
  }

  store.$subscribe((_, state) => {
    localStorage.setItem(key, JSON.stringify(state))
  })
})

Особенности реализации:

  • восстановление состояния выполняется до первого использования store;
  • $patch сохраняет реактивность;
  • сериализация должна учитывать сложные типы данных.

Работа с TypeScript (типизация плагинов)

Для корректной типизации расширений store используется декларативное расширение интерфейсов.

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomProperties {
    createdAt: number
    reset(): void
  }
}

После этого свойства и методы, добавленные плагином, будут доступны в IDE и проверяться компилятором.


Ограничения и архитектурные рекомендации

  • Плагины не должны изменять бизнес-логику store.
  • Не рекомендуется выполнять тяжёлые вычисления внутри $subscribe.
  • Один плагин — одна ответственность.
  • Побочные эффекты (I/O, storage, аналитика) допустимы, но должны быть изолированы.
  • Плагины не предназначены для замены middleware уровня роутера или HTTP-клиента.

Сравнение с Vuex-плагинами

Pinia-плагины концептуально похожи на плагины Vuex, но обладают рядом отличий:

  • прямой доступ к store без мутаций;
  • отсутствие строгого разделения mutations/actions;
  • лучшая типизация;
  • более простой API;
  • отсутствие необходимости в строковых ключах.

Pinia делает плагины более декларативными и менее связанными с внутренней реализацией хранилища.


Типовые сценарии использования

  • автоматическое логирование всех действий;
  • синхронизация с localStorage или IndexedDB;
  • глобальная обработка ошибок;
  • внедрение feature flags;
  • сбор метрик и телеметрии;
  • отладочные инструменты.

Плагины Pinia формируют уровень расширяемости, который позволяет масштабировать архитектуру состояния без усложнения самих store.