In Pinia, how do you register a store plugin with pinia.use(), and what happens to the object that plugin returns?
answer
- a function, not an install object
- called per store, not per app
- returned keys merged onto the store
- the store unwraps returned refs
- devtools custom properties list
basics
~20 sYou 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.
solid answer
~40 sA Pinia plugin is a plain function registered with `pinia.use(plugin)` on the instance from `createPinia()`. Pinia calls it for **each store** when that store is first created, passing a context, and merges whatever object it returns onto the store with `Object.assign`. Because the store is a `reactive()` object, a returned `ref` is unwrapped: `store.visits` reads the number, not the ref. A ref created inside the plugin is per store; one created outside and returned by every call is shared by all stores. Returning is preferred over assigning `store.x = ...` because, in development builds, Pinia records returned keys in `store._customProperties`, which devtools reads. To touch only some stores, check `store.$id` or a custom option and return nothing.
code
ts · 14 linesimport { ref } from 'vue'
import { createPinia } from 'pinia'
const sharedClock = ref(Date.now())
export const pinia = createPinia()
pinia.use(({ store }) => {
if (store.$id === 'session') return
return {
visits: ref(0),
clock: sharedClock,
}
})go deeper
Recall that a Pinia plugin is a function given to pinia.use(), that it runs for every store, and that returned keys appear on each store.
Explain why a returned ref reads without .value, and how the place a ref is created decides whether stores share it or each get their own.
Show you know returned properties are not state: they skip SSR serialisation and $reset, and devtools only lists them because Pinia records returned keys in development.
Weigh a global plugin against per-store code: every store pays for what a plugin adds, so decide what is truly cross-cutting and narrow the rest by id or option.
## What a Pinia plugin is A **Pinia plugin** is an ordinary function that extends stores. You register it on the root instance returned by `createPinia()` with `pinia.use(plugin)`, and Pinia calls it **once for every store it creates afterwards**, not once for the whole application. The call happens when a store is first created, which in practice is the first `useXxxStore()` call for that store id on that pinia. Each call receives a **context** object (the pinia, the app, the store being built and the options it was defined with) and may return an object of properties, or nothing. The Pinia docs list what plugins are for: - adding a property or method to every store, such as a shared service; - adding a new piece of state to every store; - reading a custom option passed to `defineStore()`; - wrapping or replacing actions, or subscribing to them; - side effects such as saving state to Web Storage; - applying any of the above to specific stores only. ## Registering it ```ts import { createApp, ref } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const pinia = createPinia() pinia.use(({ store }) => ({ lastOpenedAt: ref<Date | null>(null), describe: () => `store ${store.$id}`, })) createApp(App).use(pinia).mount('#app') ``` `pinia.use()` returns the pinia, so calls chain. Register plugins where you create the pinia, before any store is used, so every store receives them. ## Returning versus assigning A plugin can add properties in two ways, and they are not quite equivalent: | Approach | How | Devtools | Notes | |---|---|---|---| | **Return an object** | `return { hello: 'world' }` | keys recorded automatically in development | merged onto the store with `Object.assign` | | **Assign on the store** | `store.hello = 'world'` | not listed unless you record the key | add it to `store._customProperties` in development only | When a plugin returns an object, Pinia adds each returned key to `store._customProperties`, a `Set` that exists only in development builds (and in builds with devtools enabled in the browser); the devtools integration reads it to show **custom properties**. If you assign directly, the property still works, but devtools does not show it unless you add the key to that set yourself, guarded by a development check because the set is absent in production. The docs therefore recommend returning whenever you can. ## Refs, reactivity and sharing Every store is a `reactive()` object, and a `reactive()` object **unwraps refs** stored on it. A plugin that returns `{ visits: ref(0) }` therefore gives a store whose `store.visits` reads `0`, not a ref, and `store.visits++` writes through to the ref. It is the same mechanism that lets you read getters without `.value`. Where the ref is created decides who shares it: 1. A ref created **inside** the plugin function is new on every call, so each store gets its own value. 2. A ref created **outside** the plugin and handed to every store is one ref, so every store reads and writes the same value. Both are legitimate; the bug is choosing one while meaning the other. ## Returned properties are not state A property added this way lives on the store object; it is **not** part of `store.$state`. It is not serialised with `pinia.state` during server rendering, `$patch()` and `$reset()` do not manage it, and devtools lists it apart from state. Adding a real state field takes more work: it has to be written to `store.$state` as well. In development, Pinia 4 also inspects what you return. An object value that is not a ref, not reactive and not marked with `markRaw()` triggers the coded console diagnostic `PINIA_R1006`, because such a value is skipped by `storeToRefs()`. Functions and primitives pass without comment. ## Applying to only some stores A plugin runs for every store, so narrowing is the plugin's own job: check `store.$id`, or read a custom option from the context's `options`, and return nothing for stores that should be left alone. Returning `undefined` adds nothing. ## Typing the additions In TypeScript a returned property does not appear on the store's type until you augment the `PiniaCustomProperties` interface inside a `declare module 'pinia'` block. After that, `store.lastOpenedAt` is typed in every component without `any` or `@ts-ignore`. ## Common mistakes - Expecting the plugin to run once for the app rather than once per store. - Reading `store.visits.value`: the store has already unwrapped the ref. - Declaring a ref at module scope and expecting per-store values. - Assuming returned properties are serialised in SSR state or reset by `$reset()`.
- A plugin returns { visits: ref(0) }; how does a component increment it, and is the change reactive?The component writes `store.visits++`. The store is `reactive()`, so assigning a plain number to a key that holds a ref writes into that ref's `.value`; the ref stays in place and everything reading `store.visits` re-renders. Writing `store.visits.value++` would fail, because `store.visits` already reads as a number.
- Why does the Pinia docs example wrap store._customProperties.add() in a development check?`_customProperties` exists only in development builds and in builds that enable devtools in the browser; Pinia leaves it out of a plain production build. Calling `.add()` on it there would throw on `undefined`. Returning the properties from the plugin avoids the problem, because Pinia records returned keys itself, only when the set exists.
saying these in an interview costs you the question
- A Pinia plugin runs once for the whole app when pinia.use() is called
- A ref returned by a plugin must be read as store.visits.value
- Properties a plugin returns become part of store.$state and are serialised
- A ref declared at module scope gives each store its own copy
- Assigning store.x and returning x are identical as far as devtools is concerned