Sistema de plugins de Pinia
Pinia permite extender la funcionalidad de todos los stores mediante plugins. Un plugin de Pinia es una función que recibe un objeto de contexto y puede añadir propiedades, envolver acciones o suscribirse a cambios de estado.
La estructura básica de un plugin es:
import type { PiniaPlugin, PiniaPluginContext } from 'pinia'
const myPlugin: PiniaPlugin = (context: PiniaPluginContext) => {
// context.pinia - instancia de Pinia
// context.app - instancia de la app Vue
// context.store - store al que se aplica el plugin
// context.options - opciones pasadas a defineStore
}
Para registrar un plugin, se usa el método use de la instancia de Pinia:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(myPlugin)
app.use(pinia)
app.mount('#app')
Crear un plugin personalizado
Plugin de logging
Un caso de uso habitual es registrar todas las acciones que se ejecutan en cualquier store:
import type { PiniaPlugin } from 'pinia'
export const loggerPlugin: PiniaPlugin = ({ store }) => {
store.$onAction(({ name, args, after, onError }) => {
console.log(`[${store.$id}] Acción iniciada: ${name}`, args)
after((result) => {
console.log(`[${store.$id}] Acción completada: ${name}`, result)
})
onError((error) => {
console.error(`[${store.$id}] Acción fallida: ${name}`, error)
})
})
}
Plugin que añade propiedades a todos los stores
Un plugin puede retornar un objeto para añadir propiedades a cada store:
import type { PiniaPlugin } from 'pinia'
import { ref } from 'vue'
export const timestampPlugin: PiniaPlugin = ({ store }) => {
const lastUpdated = ref<Date | null>(null)
store.$subscribe(() => {
lastUpdated.value = new Date()
})
// Se retorna un objeto que se fusiona con el store
return { lastUpdated }
}
Tras registrar este plugin, todos los stores tendrán una propiedad lastUpdated accesible:
<script setup lang="ts">
import { useTaskStore } from '@/stores/taskStore'
const taskStore = useTaskStore()
// lastUpdated está disponible en todos los stores
console.log(taskStore.lastUpdated)
</script>
Plugin que envuelve acciones
Los plugins pueden modificar el comportamiento de las acciones existentes:
import type { PiniaPlugin } from 'pinia'
export const errorHandlerPlugin: PiniaPlugin = ({ store }) => {
store.$onAction(({ name, onError }) => {
onError((error) => {
// Enviar error a un servicio de monitorización
console.error(`Error en ${store.$id}.${name}:`, error)
// Se podría integrar con un servicio como Sentry
})
})
}
pinia-plugin-persistedstate
El plugin pinia-plugin-persistedstate permite persistir el estado de los stores automáticamente en localStorage, sessionStorage u otro mecanismo de almacenamiento.
Instalación
npm install pinia-plugin-persistedstate
Configuración global
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')
Configuración básica por store
Para activar la persistencia en un store, se añade la opción persist: true:
Options Store:
import { defineStore } from 'pinia'
export const useSettingsStore = defineStore('settings', {
state: () => ({
theme: 'light' as 'light' | 'dark',
language: 'es',
fontSize: 16
}),
actions: {
setTheme(theme: 'light' | 'dark') {
this.theme = theme
},
setLanguage(lang: string) {
this.language = lang
}
},
persist: true
})
Setup Store:
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSettingsStore = defineStore('settings', () => {
const theme = ref<'light' | 'dark'>('light')
const language = ref('es')
const fontSize = ref(16)
function setTheme(newTheme: 'light' | 'dark') {
theme.value = newTheme
}
function setLanguage(lang: string) {
language.value = lang
}
return { theme, language, fontSize, setTheme, setLanguage }
}, {
persist: true
})
Configuración avanzada
El plugin acepta un objeto de configuración para personalizar el comportamiento:
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
email: '',
preferences: {
notifications: true,
newsletter: false
},
sessionToken: ''
}),
persist: {
// Clave personalizada en el storage
key: 'app-user-data',
// Elegir storage (localStorage por defecto)
storage: sessionStorage,
// Persistir solo ciertas propiedades del estado
pick: ['name', 'email', 'preferences'],
// Serializador personalizado
serializer: {
serialize: JSON.stringify,
deserialize: JSON.parse
},
// Hook que se ejecuta antes de restaurar el estado
beforeHydrate: (ctx) => {
console.log(`Restaurando estado de ${ctx.store.$id}`)
},
// Hook que se ejecuta después de restaurar el estado
afterHydrate: (ctx) => {
console.log(`Estado restaurado de ${ctx.store.$id}`)
}
}
})
La opción pick es especialmente útil para evitar persistir datos sensibles como tokens de sesión, conservando solo la información que debe sobrevivir entre recargas de página.
Testing con createTestingPinia
El paquete @pinia/testing proporciona utilidades para probar stores y componentes que los utilizan. Permite crear una instancia de Pinia preparada para testing que stub (sustituye) automáticamente las acciones.
Instalación
npm install -D @pinia/testing
Configuración básica en Vitest
// tests/stores/counterStore.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { useCounterStore } from '@/stores/counterStore'
describe('counterStore', () => {
beforeEach(() => {
setActivePinia(
createTestingPinia({
createSpy: vi.fn
})
)
})
it('tiene el estado inicial correcto', () => {
const store = useCounterStore()
expect(store.count).toBe(0)
})
it('las acciones están stubbed por defecto', () => {
const store = useCounterStore()
store.increment()
// La acción se llamó pero no ejecutó la lógica real
expect(store.increment).toHaveBeenCalledTimes(1)
expect(store.count).toBe(0) // No cambió porque está stubbed
})
})
Establecer estado inicial
Se puede definir un estado inicial personalizado para las pruebas:
it('puede empezar con un estado personalizado', () => {
setActivePinia(
createTestingPinia({
createSpy: vi.fn,
initialState: {
counter: { count: 42, step: 5 }
}
})
)
const store = useCounterStore()
expect(store.count).toBe(42)
expect(store.step).toBe(5)
})
El nombre usado en initialState es el identificador pasado como primer argumento a defineStore.
Ejecutar acciones reales
Si se necesita que las acciones ejecuten su lógica real en una prueba, se puede desactivar el stubbing:
it('ejecuta acciones reales cuando se configura', () => {
setActivePinia(
createTestingPinia({
createSpy: vi.fn,
stubActions: false
})
)
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1) // La lógica real se ejecutó
})
Testing de componentes con stores
Se puede usar createTestingPinia al montar componentes con @vue/test-utils:
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import TaskList from '@/components/TaskList.vue'
import { useTaskStore } from '@/stores/taskStore'
describe('TaskList', () => {
it('muestra las tareas del store', () => {
const wrapper = mount(TaskList, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
initialState: {
task: {
tasks: [
{ id: 1, title: 'Test 1', completed: false },
{ id: 2, title: 'Test 2', completed: true }
],
isLoading: false
}
}
})
]
}
})
expect(wrapper.findAll('li')).toHaveLength(2)
})
it('llama a la acción al hacer click', async () => {
const wrapper = mount(TaskList, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
initialState: {
task: {
tasks: [
{ id: 1, title: 'Test 1', completed: false }
]
}
}
})
]
}
})
const store = useTaskStore()
await wrapper.find('button.toggle').trigger('click')
expect(store.toggleTask).toHaveBeenCalledWith(1)
})
})
Espiar acciones manteniendo la lógica real
Se puede combinar stubActions: false con createSpy para ejecutar las acciones reales y al mismo tiempo verificar que fueron llamadas:
it('ejecuta y espía acciones', () => {
setActivePinia(
createTestingPinia({
createSpy: vi.fn,
stubActions: false
})
)
const store = useCounterStore()
store.increment()
expect(store.increment).toHaveBeenCalledTimes(1)
expect(store.count).toBe(1)
})
Esta combinación es útil cuando se necesita verificar que la acción se ejecutó correctamente y que produjo el efecto esperado en el estado.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje