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:
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:
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()
:
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:
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:
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:
// 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:
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 constore.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:
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
:
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:
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:
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:
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:
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:
// 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:
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:
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
.
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:
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):
pinia.use(({ options }) => ({ $options: options }))
pinia.use(({ options }) => ({ $options: options }))
Podemos tipar esto adecuadamente usando los 4 tipos genéricos de PiniaCustomProperties
:
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:
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:
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
:
// 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:
// 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