Plugins are how Pinia extends all stores at once. This episode covers creating a plugin with pinia.use, adding global properties to stores, accepting plugin options through the options argument, and using $subscribe and $onAction for store-level observability.

Pinia is designed to be extensible. Instead of repeating the same code in every store — writing a logger, adding router access, or syncing localStorage — you can write a plugin once and apply it to all stores automatically.
Episode 8 covers Pinia's plugin mechanism: the plugin structure with pinia.use, adding global properties to stores, accepting plugin options, and the two store-level observability APIs: $subscribe and $onAction.
A plugin is a function installed via pinia.use(). It receives a single context containing pinia, app, store, and options:
export function loggerPlugin({ store }) {
store.$subscribe((mutation, state) => {
console.log(`Store ${store.$id} changed`, mutation.type, state)
})
}Plugins are installed after the Pinia instance is created:
import { createPinia } from 'pinia'
import { loggerPlugin } from '@/plugins/logger'
const pinia = createPinia()
pinia.use(loggerPlugin)
app.use(pinia)pinia.use(loggerPlugin) registers the plugin with the Pinia instance. The { store } context gives the plugin access to every newly created store, so store.$id can be read for identification.
A plugin can attach new properties to every store:
import { ref } from 'vue'
export function localSyncPlugin({ store }) {
const saved = localStorage.getItem(store.$id)
if (saved) {
store.$patch(JSON.parse(saved))
}
store.$subscribe((_mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}store.$patch(JSON.parse(saved)) hydrates state from localStorage when the store is created, and $subscribe saves subsequent changes. This plugin is the foundation of persistence, which we'll refine in episode 9.
Sometimes a plugin needs per-store configuration. Options are passed through options — the third argument of defineStore:
export function persistPlugin({ options }) {
if (options.persist === true) {
console.log(`Store will be persisted`)
}
}
// In the store definition
export const useThemeStore = defineStore('theme', {
state: () => ({ mode: 'light' }),
persist: true, // read by the plugin through options
})options.persist === true is read by the plugin from the store options. This is the pattern used by the pinia-plugin-persistedstate library — and that's how you add configuration to stores without changing Pinia's core.
Besides plugins, these two APIs can be used directly in components:
store.$subscribe listens for every state change — useful for autosave and analytics.store.$onAction listens for action calls — useful for logging and tracking.store.$subscribe((mutation, state) => {
console.log(mutation.storeId, mutation.type, state)
})
store.$onAction(({ name, args, after, onError }) => {
console.log('Action started', name, args)
after((result) => console.log('Finished', result))
onError((error) => console.error('Failed', error))
})store.$onAction(({ name, args, after, onError }) => ...) captures the action name, arguments, the result after completion, and errors. The combination of $subscribe and $onAction is the foundation of Pinia observability.
Tip
By default, $subscribe only stays active while the component that registered it exists. Pass the { detached: true } option to make it last longer — we'll cover this in episode 13.
Episode 8 opens up Pinia's plugin system. You can now write plugins with pinia.use, add global properties to all stores, read store options from options, and use $subscribe and $onAction for observability.
Key takeaways:
pinia.use.pinia, app, store, and options.defineStore.$subscribe listens for state changes.$onAction listens for action calls, including results and errors.In the next episode, episode 9, we'll discuss persistence and state storage — saving state to localStorage and sessionStorage with pinia-plugin-persistedstate, picking which fields to persist, using custom storage, and handling rehydration. We'll put the plugin from episode 8 to real use.