skip to content

Store Plugins

A Pinia plugin runs for every store and can add properties, add state or wrap actions. Interviewers probe the plugin context, why external objects need markRaw, and how to type additions.

on this pageshow

explore

questions

5

In Pinia, what does a store plugin receive in its context, and how would you use it to audit-log every action of every store?

level: middleimportance: must knowfreq 46%

answer

  1. one argument, four keys
  2. pinia, app, store, options
  3. options.actions even for setup stores
  4. runs in the store's own scope
  5. $onAction with after and onError

basics

~20 s

A Pinia plugin receives one context object with pinia, app, store and options; an audit plugin calls store.$onAction on each store and logs the action name, arguments and outcome from the after and onError hooks.

solid answer

~40 s

Pinia calls a plugin with a context of four keys: `pinia` (the root instance), `app` (the Vue app it was installed in), `store` (the store being created) and `options` (what was passed to `defineStore()`, with `options.actions` filled in even for setup stores). It runs once per store, when the store is first created, inside that store's effect scope. For audit logging I register a plugin that calls `store.$onAction()`: the callback gets the action `name` and `args` before the action runs, and I record the outcome with `after` for success and `onError` for failures, tagging each entry with `store.$id`. Plugins given to `pinia.use()` before `app.use(pinia)` are queued until installation, but a store that already exists when a plugin is added never gets it.

code

ts · 9 lines
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { auditPlugin } from './stores/audit-plugin'

const pinia = createPinia()
pinia.use(auditPlugin)

createApp(App).use(pinia).mount('#app')

go deeper

for a junior

Know the four context keys a Pinia plugin receives: pinia, app, store and options, and that the plugin runs for each store.

for a middle

Explain when plugins run: once per store at creation, queued until app.use(pinia), never retroactively, and inside the store's effect scope.

for a senior

Design the audit plugin with care: allow-list stores or actions, avoid logging sensitive arguments, keep the pre-action callback cheap and batch sends.

for a principal

Decide whether auditing belongs in the store layer at all or at the API boundary, and what the team gains or loses by making it implicit for every store.

## The plugin context Pinia calls each plugin with a single **context** object, typed `PiniaPluginContext`: | Key | What it is | Typical use | |---|---|---| | `pinia` | the root instance from `createPinia()` | reach `pinia.state`, share data between stores | | `app` | the Vue app the pinia was installed into with `app.use(pinia)` | read app-level configuration | | `store` | the store being created, already reactive | add properties, call `$subscribe()`, `$onAction()`, `$patch()` | | `options` | the options given to `defineStore()` | read custom options, list `options.actions` | `options` is the options object of an **option store**, or the third argument of a **setup store**. In both cases Pinia guarantees an `options.actions` object: for an option store it is the `actions` you wrote, and for a setup store Pinia fills it with the functions the setup function returned. A plugin can therefore see every action name whichever syntax the store uses. ## When a plugin runs 1. `pinia.use(plugin)` records the plugin. If `app.use(pinia)` has not run yet, Pinia **queues** it and activates it during installation, so registering before or after `app.use(pinia)` both work. 2. A store is created lazily, on the first `useXxxStore()` call for its id on that pinia. 3. At the end of creation, Pinia runs every active plugin in registration order, **inside the store's own effect scope**, and merges what each returns onto the store. 4. Later `useXxxStore()` calls return the cached store; plugins do not run again. Two consequences follow. A plugin added after a store already exists never reaches that store. And a store created on a pinia that was never installed in an app gets no plugins, which is a common surprise in unit tests (`createTestingPinia()` from `@pinia/testing` takes a `plugins` list for this). ## An audit-logging plugin Audit logging must observe every action of every store, which is what `store.$onAction()` provides. Its full behaviour belongs to action hooks; what the plugin needs is that the callback runs **before** each action with the action's `name` and `args`, plus two hooks for the outcome. ```ts import type { PiniaPluginContext } from 'pinia' import { sendAuditEntry } from '@/services/audit' export function auditPlugin({ store }: PiniaPluginContext) { if (store.$id === 'session') return store.$onAction(({ name, args, after, onError }) => { const startedAt = performance.now() after(() => { sendAuditEntry({ store: store.$id, action: name, args, ok: true, ms: performance.now() - startedAt }) }) onError((error) => { sendAuditEntry({ store: store.$id, action: name, args, ok: false, error: String(error) }) }) }) } ``` What each piece relies on: - `store.$id` tags each entry and lets the plugin skip a store whose arguments carry credentials. - `after` receives the action's return value, and for an async action it waits until the promise resolves, so durations cover the real work. - `onError` runs when the action throws or its promise rejects. Pinia still rethrows the error to the caller; the plugin only observes it. - The subscription is created inside the store's effect scope, so it lives as long as the store, not as long as the component that happened to call `useXxxStore()` first. ## Using the other keys - `options.actions` lets a plugin decide per action, for example logging only actions whose names appear in a custom option. - `pinia` gives access to `pinia.state`, the root state of every store, when an entry needs context from elsewhere. - `app` is useful when a plugin needs the application's configuration; it is the same app `pinia` was installed in. ## Design notes for audit logging - Log arguments deliberately: action arguments may include personal data, so allow-list stores or actions rather than logging everything blindly. - Keep the callback cheap: it runs synchronously before every action, so batch network sends rather than awaiting them. - Register the plugin once, where the pinia is created, so every store created afterwards is covered. - Decide what an entry means for nested calls: an action that calls another through the store (`this.save()` in an option store) produces two entries, while a setup store calling its own local function directly produces one, because only the copy on the store is wrapped. - Remember the plugin runs wherever stores are created, including during server rendering, so the sending function must work there or be skipped. ## Common mistakes - Expecting Vuex-style `state`, `getters`, `commit` and `dispatch` in the context. - Adding the plugin after stores were already used and wondering why they are not logged. - Assuming `options.actions` is empty for setup stores.

  • An audit entry's onError callback returns false; does the failing action still reject for its caller?
    Yes. In Pinia 4.0.3 the action wrapper calls every `onError` callback and then rethrows the error, or returns a rejected promise for an async action, regardless of what the callbacks return. An audit plugin observes failures; it cannot swallow them.
  • How can the audit plugin log only the actions a store author opts into?
    Read a custom option from the context: the store passes something like `audit: ['checkout']` in its options (the third argument for a setup store), the plugin checks `options.audit` and compares it with the `name` in `$onAction`. Type the option by augmenting `DefineStoreOptionsBase` so TypeScript accepts it.
  • Why does the $onAction subscription made in a plugin survive the unmount of the component that first used the store?
    Pinia runs plugins inside the store's own effect scope, so a subscription created there is tied to the store, not to the component whose setup triggered creation. It is removed when the store is disposed with `$dispose()`, not when that component unmounts.

saying these in an interview costs you the question

  • The plugin context holds state, getters, commit and dispatch as in Vuex
  • A plugin added later is applied to stores that already exist
  • Calling pinia.use() before app.use(pinia) silently drops the plugin
  • An $onAction made in a plugin stops when the first component unmounts
  • options.actions is always empty for setup stores
  • Returning false from onError stops the error reaching the caller
open as a page

In Pinia, how do you register a store plugin with pinia.use(), and what happens to the object that plugin returns?

level: juniorimportance: should knowfreq 41%

basics

~20 s

You pass a function to pinia.use(); Pinia calls it once for every store created afterwards and merges the returned object's keys onto that store, where refs are unwrapped and, in development, devtools lists the keys as custom properties.

open as a page

A Pinia plugin returns { hasError: ref(false) } for every store; why is hasError missing from SSR state and $reset, and how should it be added?

level: seniorimportance: should knowfreq 28%

basics

~20 s

A returned ref is only a store property: it never enters store.$state, so SSR does not serialise it and $reset ignores it. Add real state by writing a ref to store.$state when absent, then linking store.hasError with toRef.

open as a page

In a Pinia plugin that adds the Vue Router instance to every store, why wrap it in markRaw(), and what breaks without it?

level: seniorimportance: should knowfreq 33%

basics

~10 s

Every store is reactive(), so an external object put on it is read back through a reactive proxy that unwraps its refs; store.router.currentRoute.value then fails. markRaw() makes the store hand back the untouched router.

open as a page

How would you build a Pinia plugin that persists only stores declaring a persist option, such as a theme store and a form draft, and type that option?

level: seniorimportance: should knowfreq 37%

basics

~20 s

Stores opt in with a custom persist option, the third defineStore argument for setup stores; the plugin reads options.persist, restores saved state with $patch, saves on $subscribe, skips the server, and DefineStoreOptionsBase types the option.

open as a page