Skip to content
En esta página

Plugins

Los almacenes de Pinia pueden ser completamente extensibles gracias a una API de bajo nivel. Aquí tienes una lista de cosas que puedes hacer:

  • Añadir nuevas propiedades a los almacenes
  • Añadir nuevas opciones cuando defines almacenes
  • Añadir nuevos métodos a los almacenes
  • Envolver métodos existentes
  • Interceptar acciones y sus resultados
  • Implementar efectos secundarios como Local Storage
  • Aplicar solo a almacenes específicos

Los plugins son añadidos a la instancia de pinia con pinia.use(). El ejemplo más sencillo es añadir una propiedad estática a todos los almacenes mediante el retorno de un objeto:

js
import { createPinia } from 'pinia'  // añadir una propiedad llamada `secret` a todos los almacenes creados después  // de que este plugin este instalado. Esto puede estar en otro archivo  // diferente function SecretPiniaPlugin() {  return { secret: 'the cake is a lie' } }  const pinia = createPinia() // le damos el plugin a pinia pinia.use(SecretPiniaPlugin)  // en otro archivo const store = useStore() store.secret // 'the cake is a lie'
import { createPinia } from 'pinia'  // añadir una propiedad llamada `secret` a todos los almacenes creados después  // de que este plugin este instalado. Esto puede estar en otro archivo  // diferente function SecretPiniaPlugin() {  return { secret: 'the cake is a lie' } }  const pinia = createPinia() // le damos el plugin a pinia pinia.use(SecretPiniaPlugin)  // en otro archivo const store = useStore() store.secret // 'the cake is a lie'

Esto es útil para añadir objetos globales como router, modal o toast managers.

Introducción

Un plugin de Pinia es una función que opcionalmente retorna propiedades para ser añadidas a un almacén. Toma un argumento opcional, un contexto:

js
export function myPiniaPlugin(context) {  context.pinia // el almacén creado con `createPinia()`  context.app // la aplicación actual creada con `createApp()` (solo Vue 3)  context.store // el almacén aumentado por el plugin  context.options // el objeto de opciones que define al almacén pasado por `defineStore()`  // ... }
export function myPiniaPlugin(context) {  context.pinia // el almacén creado con `createPinia()`  context.app // la aplicación actual creada con `createApp()` (solo Vue 3)  context.store // el almacén aumentado por el plugin  context.options // el objeto de opciones que define al almacén pasado por `defineStore()`  // ... }

Esta función es pasada a pinia con pinia.use():

js
pinia.use(myPiniaPlugin)
pinia.use(myPiniaPlugin)

Los plugins son solo aplicadas a almacenes creados después de los propios plugins, y después de que pinia sea pasada a la aplicación, de lo contrario no serán aplicados.

Aumentar un Almacén

Puedes añadir propiedades a cualquier almacén solo retornando un objeto de estas mismas en un plugin:

js
pinia.use(() => ({ hello: 'world' }))
pinia.use(() => ({ hello: 'world' }))

También puedes colocar la propiedad directamente en la store pero si es posible usa la versión retornada para que pueda ser seguida automáticamente por las herramientas de desarrollo:

js
pinia.use(({ store }) => {  store.hello = 'world' })
pinia.use(({ store }) => {  store.hello = 'world' })

Cualquier propiedad retornada por un plugin será automáticamente seguido por las herramientas de desarrollo para hacer que hello sea visible en las herramientas de desarrollo. Asegúrate de añadirlo a store._customProperties solo en modo de desarrollo si quieres depurarlo en las herramientas de desarrollo:

js
// del ejemplo anterior pinia.use(({ store }) => {  store.hello = 'world'  // asegúrate que tu bundler maneje esto. webpack y vite deberían hacerlo por defecto  if (process.env.NODE_ENV === 'development') {  // añade cualquier clave que crees al almacén  store._customProperties.add('hello')  } })
// del ejemplo anterior pinia.use(({ store }) => {  store.hello = 'world'  // asegúrate que tu bundler maneje esto. webpack y vite deberían hacerlo por defecto  if (process.env.NODE_ENV === 'development') {  // añade cualquier clave que crees al almacén  store._customProperties.add('hello')  } })

Cabe aclarar que cualquier almacén está envuelto con reactive, por lo que desenvuelve automáticamente cualquier Ref (ref(), computed(), ...) que este contiene:

js
const sharedRef = ref('shared') pinia.use(({ store }) => {  // cada almacén tiene sus propias propiedades `hello`  store.hello = ref('secret')  // se desenvuelven automáticamente  store.hello // 'secret'   // todos los almacenes comparten el valor de la propiedad `shared`  store.shared = sharedRef  store.shared // 'shared' })
const sharedRef = ref('shared') pinia.use(({ store }) => {  // cada almacén tiene sus propias propiedades `hello`  store.hello = ref('secret')  // se desenvuelven automáticamente  store.hello // 'secret'   // todos los almacenes comparten el valor de la propiedad `shared`  store.shared = sharedRef  store.shared // 'shared' })

Por eso se puede acceder a todas las propiedades computadas sin .value y por eso son reactivas.

Añadir nuevo estado

Si quieres añadir nuevas propiedades de estado a un almacén o propiedades que están hechas para ser usadas durante la hidratación, tendrás que añadirlas en dos lugares:

  • En store para que puedas acceder a ella con store.myState
  • En store.$state para que pueda ser usada en las devtools y ser serializada durante SSR

Además de eso, seguramente tendrás que usar un ref() (u otra API reactiva) para poder compartir el valor entre los distintos accesos:

js
import { toRef, ref } from 'vue'  pinia.use(({ store }) => {  // para manejar correctamente SSR, necesitarás estar seguro de no  // sobrescribir un valor existente  if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {  // hasError está definido en el plugin, por lo que cada almacén tiene  // sus propias propiedades de estado  const hasError = ref(false)  // establecer la variable en `$state` le permitirá ser serializada  // durante SSR  store.$state.hasError = hasError  }  // necesitamos transferir el ref del estado al almacén, así ambos  // accesos: store.hasError y store.$state.hasError funcionarán y  // compartirán la misma variable  // Mira https://vuejs.org/api/reactivity-utilities.html#toref  store.hasError = toRef(store.$state, 'hasError')   // en este caso es mejor no devolver `hasError` debido a que  // será mostrada igualmente en la sección `estado` en las   // herramientas de desarrollo y si la retornamos, las   // herramientas de desarrollo la mostrarán dos veces. })
import { toRef, ref } from 'vue'  pinia.use(({ store }) => {  // para manejar correctamente SSR, necesitarás estar seguro de no  // sobrescribir un valor existente  if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {  // hasError está definido en el plugin, por lo que cada almacén tiene  // sus propias propiedades de estado  const hasError = ref(false)  // establecer la variable en `$state` le permitirá ser serializada  // durante SSR  store.$state.hasError = hasError  }  // necesitamos transferir el ref del estado al almacén, así ambos  // accesos: store.hasError y store.$state.hasError funcionarán y  // compartirán la misma variable  // Mira https://vuejs.org/api/reactivity-utilities.html#toref  store.hasError = toRef(store.$state, 'hasError')   // en este caso es mejor no devolver `hasError` debido a que  // será mostrada igualmente en la sección `estado` en las   // herramientas de desarrollo y si la retornamos, las   // herramientas de desarrollo la mostrarán dos veces. })

Cabe aclarar que los cambios o adiciones al estado que ocurren en un plugin (esto incluye la llamada a store.$patch()) se realizan antes de que el almacén este activo y por tanto no dispara ninguna suscripción.

WARNING

Si estas usando Vue 2, Pinia esta sujeto a las mismas advertencias de reactividad como Vue. Necesitarás usar Vue.set() (Vue 2.7) o set() (desde @vue/composition-api para Vue <2.7) para cuando crees nuevas propiedades de estado como secret y hasError:

js
import { set, toRef } from '@vue/composition-api' pinia.use(({ store }) => {  if (!Object.prototype.hasOwnProperty(store.$state, 'secret')) {  const secretRef = ref('secret')  // Si los datos están pensado para ser usados durante SSR  // deberás colocarlos en la propiedad `$state` para que  // se serialicen y se usen durante la hidratación  set(store.$state, 'secret', secretRef)  }  // ponlos directamente en el almacén también para que puedas  // acceder a estos de las dos formas: `store.$state.secret`   // / `store.secret`  set(store, 'secret', toRef(store.$state, 'secret'))  store.secret // 'secret' })
import { set, toRef } from '@vue/composition-api' pinia.use(({ store }) => {  if (!Object.prototype.hasOwnProperty(store.$state, 'secret')) {  const secretRef = ref('secret')  // Si los datos están pensado para ser usados durante SSR  // deberás colocarlos en la propiedad `$state` para que  // se serialicen y se usen durante la hidratación  set(store.$state, 'secret', secretRef)  }  // ponlos directamente en el almacén también para que puedas  // acceder a estos de las dos formas: `store.$state.secret`   // / `store.secret`  set(store, 'secret', toRef(store.$state, 'secret'))  store.secret // 'secret' })

Resetear el estado añadido en plugins

Por defecto, $reset() no reseteará el estado añadido por los plugins pero puedes sobreescribirlo para resetear el estado que añades:

js
import { toRef, ref } from 'vue' pinia.use(({ store }) => {  // esto es el mismo código que el ejemplo anterior para referencia  if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {  const hasError = ref(false)  store.$state.hasError = hasError  }  store.hasError = toRef(store.$state, 'hasError')  // asegúrate de establecer el contexto (`this`) en el almacén  const originalReset = store.$reset.bind(store)  // sobreescribe la función $reset  return {  $reset() {  originalReset()  store.hasError = false  }  } })
import { toRef, ref } from 'vue' pinia.use(({ store }) => {  // esto es el mismo código que el ejemplo anterior para referencia  if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {  const hasError = ref(false)  store.$state.hasError = hasError  }  store.hasError = toRef(store.$state, 'hasError')  // asegúrate de establecer el contexto (`this`) en el almacén  const originalReset = store.$reset.bind(store)  // sobreescribe la función $reset  return {  $reset() {  originalReset()  store.hasError = false  }  } })

Añadir nuevas propiedades externas

Cuando añadas nuevas propiedades externas, instancias de clase que vengan de otras librerías o simplemente cosas que no sean reactivas, deberás envolverlos en el objeto con markRaw() antes de pasárselos a pinia. Aquí tienes un ejemplo añadiendo el router a todos los almacén:

js
import { markRaw } from 'vue' // adapta esto según donde esté tu router import { router } from './router'  pinia.use(({ store }) => {  store.router = markRaw(router) })
import { markRaw } from 'vue' // adapta esto según donde esté tu router import { router } from './router'  pinia.use(({ store }) => {  store.router = markRaw(router) })

Llamar a $subscribe dentro de plugins

También puedes usar store.$subscribe y store.$onAction dentro de plugins:

ts
pinia.use(({ store }) => {  store.$subscribe(() => {  // reacciona a cambios del almacén  })  store.$onAction(() => {  // reacciona a acciones del almacén  }) })
pinia.use(({ store }) => {  store.$subscribe(() => {  // reacciona a cambios del almacén  })  store.$onAction(() => {  // reacciona a acciones del almacén  }) })

Añadir nuevas opciones

Es posible crear nuevas opciones cuando definas almacenes para luego consumirlas desde los plugins. Por ejemplo, puedes crear una opción debounce que te permita retrasar cualquier acción:

js
defineStore('search', {  actions: {  searchContacts() {  // ...  },  },   // Esto será leído por un plugin más adelante  debounce: {  // retrasa la acción searchContact durante 300ms  searchContacts: 300,  }, })
defineStore('search', {  actions: {  searchContacts() {  // ...  },  },   // Esto será leído por un plugin más adelante  debounce: {  // retrasa la acción searchContact durante 300ms  searchContacts: 300,  }, })

Entonces el plugin puede leer la opción para envolver las acciones y reemplazar las originales:

js
// usa cualquier librería de retrasos import debounce from 'lodash/debounce'  pinia.use(({ options, store }) => {  if (options.debounce) {  // estamos sobrescribiendo las acciones con las nuevas  return Object.keys(options.debounce).reduce((debouncedActions, action) => {  debouncedActions[action] = debounce(  store[action],  options.debounce[action]  )  return debouncedActions  }, {})  } })
// usa cualquier librería de retrasos import debounce from 'lodash/debounce'  pinia.use(({ options, store }) => {  if (options.debounce) {  // estamos sobrescribiendo las acciones con las nuevas  return Object.keys(options.debounce).reduce((debouncedActions, action) => {  debouncedActions[action] = debounce(  store[action],  options.debounce[action]  )  return debouncedActions  }, {})  } })

Cabe aclara que las opciones personalizadas son pasadas como tercer argumento cuando se usa la sintaxis de configuración:

js
defineStore(  'search',  () => {  // ...  },  {  // Esto será leído por un plugin más adelante  debounce: {  // retrasa la acción searchContact durante 300ms  searchContacts: 300,  },  } )
defineStore(  'search',  () => {  // ...  },  {  // Esto será leído por un plugin más adelante  debounce: {  // retrasa la acción searchContact durante 300ms  searchContacts: 300,  },  } )

TypeScript

Todo lo mostrado arriba puede ser hecho con soporte de tipado, así que no necesitas usar nunca más ni any ni @ts-ignore.

Tipar plugins

Un plugin de Pinia puede ser tipado de la siguiente forma:

ts
import { PiniaPluginContext } from 'pinia'  export function myPiniaPlugin(context: PiniaPluginContext) {  // ... }
import { PiniaPluginContext } from 'pinia'  export function myPiniaPlugin(context: PiniaPluginContext) {  // ... }

Tipar nuevas propiedades de almacenes

Cuando añadas nuevas propiedades al almacén, deberías hacer uso de la interfaz PiniaCustomProperties.

ts
import 'pinia' import type { Router } from 'vue-router'  declare module 'pinia' {  export interface PiniaCustomProperties {  // usando un setter podemos permitir el uso de strings y refs  set hello(value: string | Ref<string>)  get hello(): string   // también puedes definir valores simples  simpleNumber: number   // tipa el router añadido por el plugin anterior (#adding-new-external-properties)  router: Router  } }
import 'pinia' import type { Router } from 'vue-router'  declare module 'pinia' {  export interface PiniaCustomProperties {  // usando un setter podemos permitir el uso de strings y refs  set hello(value: string | Ref<string>)  get hello(): string   // también puedes definir valores simples  simpleNumber: number   // tipa el router añadido por el plugin anterior (#adding-new-external-properties)  router: Router  } }

Puede ser escrito y leído de forma segura:

ts
pinia.use(({ store }) => {  store.hello = 'Hola'  store.hello = ref('Hola')   store.simpleNumber = Math.random()  // @ts-expect-error: no hemos tipado esto correctamente  store.simpleNumber = ref(Math.random()) })
pinia.use(({ store }) => {  store.hello = 'Hola'  store.hello = ref('Hola')   store.simpleNumber = Math.random()  // @ts-expect-error: no hemos tipado esto correctamente  store.simpleNumber = ref(Math.random()) })

PiniaCustomProperties es un tipo genérico que te permite referenciar propiedades de un almacén. Imagina el siguiente ejemplo donde copiaremos las opciones iniciales como $options (esto solo funcionaría en almacenes de opciones):

ts
pinia.use(({ options }) => ({ $options: options }))
pinia.use(({ options }) => ({ $options: options }))

Podemos tipar esto adecuadamente usando los 4 tipos genéricos de PiniaCustomProperties:

ts
import 'pinia'  declare module 'pinia' {  export interface PiniaCustomProperties<Id, S, G, A> {  $options: {  id: Id  state?: () => S  getters?: G  actions?: A  }  } }
import 'pinia'  declare module 'pinia' {  export interface PiniaCustomProperties<Id, S, G, A> {  $options: {  id: Id  state?: () => S  getters?: G  actions?: A  }  } }

TIP

Cuando extiendes los tipos en genéricos, estos deben ser nombrados exactamente como en el código fuente. Id no puede ser llamado id o I, y S no puede ser llamado Estado. Aquí tienes lo que significa cada letra:

  • S: Estado (State)
  • G: Getters
  • A: Acciones
  • SS: Almacenes de configuración / Almacenes (Setup Store / Store)

Tipar nuevo estado

Cuando añades nuevas propiedades (a ambos sitios, el almacén and almacén.$state), necesitar añadir el tipo a PiniaCustomStateProperties en su lugar. A diferencia de PiniaCustomProperties, solo recibe el Estado genérico:

ts
import 'pinia'  declare module 'pinia' {  export interface PiniaCustomStateProperties<S> {  hello: string  } }
import 'pinia'  declare module 'pinia' {  export interface PiniaCustomStateProperties<S> {  hello: string  } }

Tipar nuevas opciones de creación

Al crear nuevas opciones para defineStore(), debe extender la DefineStoreOptionsBase. A diferencia de PiniaCustomProperties, sólo expone dos genéricos: el Estado y el tipo de Almacén, lo que le permite limitar lo que se puede definir. Por ejemplo, puede utilizar los nombres de las acciones:

ts
import 'pinia'  declare module 'pinia' {  export interface DefineStoreOptionsBase<S, Store> {  // permite definir un número de ms para cada una de las acciones  debounce?: Partial<Record<keyof StoreActions<Store>, number>>  } }
import 'pinia'  declare module 'pinia' {  export interface DefineStoreOptionsBase<S, Store> {  // permite definir un número de ms para cada una de las acciones  debounce?: Partial<Record<keyof StoreActions<Store>, number>>  } }

TIP

También hay un tipo StoreGetters para extraer los getters del tipo Almacén. También puedes extender las opciones de los almacenes de configuración o almacenes de opciones solo extendiendo los tipos DefineStoreOptions y DefineSetupStoreOptions respectivamente.

Nuxt.js

Cuando usas pinia junto con Nuxt, primero tendrás que crear un plugin de Nuxt. Esto te dará acceso a la instancia de pinia:

ts
// plugins/myPiniaPlugin.ts import { PiniaPluginContext } from 'pinia'  function MyPiniaPlugin({ store }: PiniaPluginContext) {  store.$subscribe((mutation) => {  // reacciona a cambios en el almacén  console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)  })   // Cabe aclara que esto se tiene que tipar si usas TS  return { creationTime: new Date() } }  export default defineNuxtPlugin(({ $pinia }) => {  $pinia.use(MyPiniaPlugin) })
// plugins/myPiniaPlugin.ts import { PiniaPluginContext } from 'pinia'  function MyPiniaPlugin({ store }: PiniaPluginContext) {  store.$subscribe((mutation) => {  // reacciona a cambios en el almacén  console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)  })   // Cabe aclara que esto se tiene que tipar si usas TS  return { creationTime: new Date() } }  export default defineNuxtPlugin(({ $pinia }) => {  $pinia.use(MyPiniaPlugin) })

Cabe aclarar que el ejemplo de arriba está usando TypeScript, tienes que eliminar las anotaciones de tipos PiniaPluginContext y Plugin junto con sus imports si estás usando un archivo .js.

Nuxt.js 2

Si estás usando Nuxt.js 2, los tipos son un poco diferentes:

ts
// plugins/myPiniaPlugin.ts import { PiniaPluginContext } from 'pinia' import { Plugin } from '@nuxt/types'  function MyPiniaPlugin({ store }: PiniaPluginContext) {  store.$subscribe((mutation) => {  // reacciona a cambios en el almacén  console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)  })   // Cabe aclara que esto se tiene que tipar si usas TS  return { creationTime: new Date() } }  const myPlugin: Plugin = ({ $pinia }) => {  $pinia.use(MyPiniaPlugin) }  export default myPlugin
// plugins/myPiniaPlugin.ts import { PiniaPluginContext } from 'pinia' import { Plugin } from '@nuxt/types'  function MyPiniaPlugin({ store }: PiniaPluginContext) {  store.$subscribe((mutation) => {  // reacciona a cambios en el almacén  console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)  })   // Cabe aclara que esto se tiene que tipar si usas TS  return { creationTime: new Date() } }  const myPlugin: Plugin = ({ $pinia }) => {  $pinia.use(MyPiniaPlugin) }  export default myPlugin