Pinia: plugins y persistencia

Intermedio
Vuejs
Vuejs
Actualizado: 27/03/2026

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 - Autor del tutorial

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