Plugins aren't just for persistence. This episode covers $onAction for tracking action calls, $subscribe that writes state to JSON, and global plugins for analytics and error handlers. You also learn to extend stores with global helpers and external library integration.

Episode 8 introduced the plugin basics. Now we level up: plugins for analytics, error handling, and logging — the things you don't want to rewrite in every store. It's these advanced plugins that make application observability run without cluttering store code.
Episode 18 covers $onAction for tracking, $subscribe that writes state to JSON, global plugins for analytics and error handlers, and how to extend stores with global helpers.
$onAction listens to all action calls in a store — including arguments, results, and errors:
export function actionTrackerPlugin({ store }) {
store.$onAction(({ name, args, after, onError }) => {
console.time(`action:${name}`)
after((result) => {
console.timeEnd(`action:${name}`)
console.log('Result', name, result)
})
onError((error) => {
console.error('Action failed', name, error)
})
})
}store.$onAction(({ name, args, after, onError }) => ...) gives complete hooks for the action lifecycle. after is called when the action succeeds, onError when it fails. This is the foundation for building loggers or action performance metrics.
$subscribe can be used to periodically save state snapshots, for example for debugging sessions:
let terakhir = 0
store.$subscribe((_mutation, state) => {
const sekarang = Date.now()
if (sekarang - terakhir > 5000) {
terakhir = sekarang
localStorage.setItem(
`snapshot-${store.$id}`,
JSON.stringify(state),
)
}
})JSON.stringify(state) inside the subscriber creates a state snapshot that can be analyzed for bug reproduction. The time limit prevents snapshots from flooding storage.
A single plugin can handle analytics for every store:
export function analyticsPlugin({ store }) {
store.$onAction(({ name, after }) => {
after(() => {
trackEvent('store_action', { store: store.$id, action: name })
})
})
}trackEvent('store_action', ...) sends the store name and action every time an action succeeds. The marketing or monitoring team only needs to read this event, with no changes in any component.
Errors that occur in actions can be centralized in one place:
export function errorHandlerPlugin({ store }) {
store.$onAction(({ onError }) => {
onError((error) => {
reportError(error, { storeId: store.$id })
})
})
}
function reportError(error: unknown, info: object) {
console.error('Error from store', info, error)
}store.$onAction(({ onError }) => ...) captures errors from all of a store's actions. With this plugin, error handling is written once and centralized — not scattered across every action.
Plugins can also attach helpers used by all stores:
pinia.use(({ store }) => {
store.$error = (message: string) => {
console.error(`[${store.$id}]`, message)
}
})store.$error = (message) => ... adds a helper method to every store. To make TypeScript recognize it, use PiniaCustomProperties as discussed in episode 16.
Warning
Choose between plugins or manual actions wisely. Use plugins for things that are uniform across stores; use actions for domain-specific logic. Don't force everything into plugins.
Episode 18 refines your plugin capabilities. You can now use $onAction for tracking and error handling, write state snapshots to JSON, build global analytics and error handler plugins, and add helpers to all stores.
Key takeaways:
$onAction provides before, after, and onError hooks for every action.$subscribe can write state snapshots to storage.store.$name.In the next episode, episode 19, we'll discuss scaling store architecture — the stores/ folder structure per feature, action and getter naming conventions, splitting large stores, and code-splitting with dynamic store registration for large-scale applications.