chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
@@ -0,0 +1,61 @@
---
name: hot-module-replacement
description: Enable HMR to preserve store state during development
---
# Hot Module Replacement (HMR)
Pinia supports HMR to edit stores without page reload, preserving existing state.
## Setup
Add this snippet after each store definition:
```ts
import { defineStore, acceptHMRUpdate } from 'pinia';
export const useAuth = defineStore('auth', {
// store options...
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot));
}
```
## Setup Store Example
```ts
import { defineStore, acceptHMRUpdate } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot));
}
```
## Bundler Support
- **Vite:** Officially supported via `import.meta.hot`
- **Webpack:** Uses `import.meta.webpackHot`
- Any bundler implementing the `import.meta.hot` spec should work
## Nuxt
With `@pinia/nuxt`, `acceptHMRUpdate` is auto-imported but you still need to add the HMR snippet manually.
## Benefits
- Edit store logic without losing state
- Add/remove state, actions, and getters on the fly
- Faster development iteration
<!--
Source references:
- https://pinia.vuejs.org/cookbook/hot-module-replacement.html
-->
@@ -0,0 +1,121 @@
---
name: nuxt-integration
description: Using Pinia with Nuxt - auto-imports, SSR, and best practices
---
# Nuxt Integration
Pinia works seamlessly with Nuxt 3/4, handling SSR, serialization, and XSS protection automatically.
## Installation
```bash
npx nuxi@latest module add pinia
```
This installs both `@pinia/nuxt` and `pinia`. If `pinia` isn't installed, add it manually.
> **npm users:** If you get `ERESOLVE unable to resolve dependency tree`, add to `package.json`:
>
> ```json
> "overrides": { "vue": "latest" }
> ```
## Configuration
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
});
```
## Auto Imports
These are automatically available:
- `usePinia()` - get pinia instance
- `defineStore()` - define stores
- `storeToRefs()` - extract reactive refs
- `acceptHMRUpdate()` - HMR support
**All stores in `app/stores/` (Nuxt 4) or `stores/` are auto-imported.**
### Custom Store Directories
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
storesDirs: ['./stores/**', './custom-folder/stores/**'],
},
});
```
## Fetching Data in Pages
Use `callOnce()` for SSR-friendly data fetching:
```vue
<script setup>
const store = useStore();
// Run once, data persists across navigations
await callOnce('user', () => store.fetchUser());
</script>
```
### Refetch on Navigation
```vue
<script setup>
const store = useStore();
// Refetch on every navigation (like useFetch)
await callOnce('user', () => store.fetchUser(), { mode: 'navigation' });
</script>
```
## Using Stores Outside Components
In navigation guards, middlewares, or other stores, pass the `pinia` instance:
```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const nuxtApp = useNuxtApp();
const store = useStore(nuxtApp.$pinia);
if (to.meta.requiresAuth && !store.isLoggedIn) {
return navigateTo('/login');
}
});
```
Most of the time, you don't need this - just use stores in components or other injection-aware contexts.
## Pinia Plugins with Nuxt
Create a Nuxt plugin:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia';
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`);
});
return { creationTime: new Date() };
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin);
});
```
<!--
Source references:
- https://pinia.vuejs.org/ssr/nuxt.html
-->
@@ -0,0 +1,121 @@
---
name: server-side-rendering
description: SSR setup, state hydration, and avoiding cross-request state pollution
---
# Server Side Rendering (SSR)
Pinia works with SSR when stores are called at the top of `setup`, getters, or actions.
> **Using Nuxt?** See the [Nuxt integration](advanced-nuxt.md) instead.
## Basic Usage
```vue
<script setup>
// ✅ Works - pinia knows the app context in setup
const main = useMainStore();
</script>
```
## Using Store Outside setup()
Pass the `pinia` instance explicitly:
```ts
const pinia = createPinia();
const app = createApp(App);
app.use(router);
app.use(pinia);
router.beforeEach((to) => {
// ✅ Pass pinia for correct SSR context
const main = useMainStore(pinia);
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login';
}
});
```
## serverPrefetch()
Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia);
return store.fetchData();
},
};
```
## onServerPrefetch()
Works normally:
```vue
<script setup>
const store = useStore();
onServerPrefetch(async () => {
await store.fetchData();
});
</script>
```
## State Hydration
Serialize state on server and hydrate on client.
### Server Side
Use [devalue](https://github.com/Rich-Harris/devalue) for XSS-safe serialization:
```ts
import devalue from 'devalue';
import { createPinia } from 'pinia';
const pinia = createPinia();
const app = createApp(App);
app.use(router);
app.use(pinia);
// After rendering, state is available
const serializedState = devalue(pinia.state.value);
// Inject into HTML as global variable
```
### Client Side
Hydrate before any `useStore()` call:
```ts
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
// Hydrate from serialized state (e.g., from window.__pinia)
if (typeof window !== 'undefined') {
pinia.state.value = JSON.parse(window.__pinia);
}
```
## SSR Examples
- [Vitesse template](https://github.com/antfu/vitesse/blob/main/src/modules/pinia.ts)
- [vite-plugin-ssr](https://vite-plugin-ssr.com/pinia)
## Key Points
1. Call stores inside functions, not at module scope
2. Pass `pinia` instance when using stores outside components in SSR
3. Hydrate state before calling any `useStore()`
4. Use `devalue` or similar for safe serialization
5. Avoid cross-request state pollution by creating fresh pinia per request
<!--
Source references:
- https://pinia.vuejs.org/ssr/
-->
@@ -0,0 +1,119 @@
---
name: using-stores-outside-components
description: Correctly using stores in navigation guards, plugins, and other non-component contexts
---
# Using Stores Outside Components
Stores need the `pinia` instance, which is automatically injected in components. Outside components, you may need to provide it manually.
## Single Page Applications
Call stores **after** pinia is installed:
```ts
import { useUserStore } from '@/stores/user';
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';
// ❌ Fails - pinia not created yet
const userStore = useUserStore();
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
// ✅ Works - pinia is active
const userStore = useUserStore();
```
## Navigation Guards
**Wrong:** Call at module level
```ts
import { createRouter } from 'vue-router';
const router = createRouter({
/* ... */
});
// ❌ May fail depending on import order
const store = useUserStore();
router.beforeEach((to) => {
if (store.isLoggedIn) {
/* ... */
}
});
```
**Correct:** Call inside the guard
```ts
router.beforeEach((to) => {
// ✅ Called after pinia is installed
const store = useUserStore();
if (to.meta.requiresAuth && !store.isLoggedIn) {
return '/login';
}
});
```
## SSR Applications
Always pass the `pinia` instance to `useStore()`:
```ts
const pinia = createPinia();
const app = createApp(App);
app.use(router);
app.use(pinia);
router.beforeEach((to) => {
// ✅ Pass pinia instance
const main = useMainStore(pinia);
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login';
}
});
```
## serverPrefetch()
Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia);
return store.fetchData();
},
};
```
## onServerPrefetch()
Works normally in `<script setup>`:
```vue
<script setup>
const store = useStore();
onServerPrefetch(async () => {
// ✅ Just works
await store.fetchData();
});
</script>
```
## Key Takeaway
Defer `useStore()` calls to functions that run after pinia is installed, rather than calling at module scope.
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/outside-component-usage.html
-->
@@ -0,0 +1,217 @@
---
name: testing
description: Unit testing stores and components with @pinia/testing
---
# Testing Stores
## Unit Testing Stores
Create a fresh pinia instance for each test:
```ts
import { setActivePinia, createPinia } from 'pinia';
import { useCounterStore } from '../src/stores/counter';
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('increments', () => {
const counter = useCounterStore();
expect(counter.n).toBe(0);
counter.increment();
expect(counter.n).toBe(1);
});
});
```
### With Plugins
```ts
import { setActivePinia, createPinia } from 'pinia';
import { createApp } from 'vue';
import { somePlugin } from '../src/stores/plugin';
const app = createApp({});
beforeEach(() => {
const pinia = createPinia().use(somePlugin);
app.use(pinia);
setActivePinia(pinia);
});
```
## Testing Components
Install `@pinia/testing`:
```bash
npm i -D @pinia/testing
```
Use `createTestingPinia()`:
```ts
import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { useSomeStore } from '@/stores/myStore';
const wrapper = mount(Counter, {
global: {
plugins: [createTestingPinia()],
},
});
const store = useSomeStore();
// Manipulate state directly
store.name = 'new name';
store.$patch({ name: 'new name' });
// Actions are stubbed by default
store.someAction();
expect(store.someAction).toHaveBeenCalledTimes(1);
```
## Initial State
Set initial state for tests:
```ts
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
initialState: {
counter: { n: 20 }, // Store name → initial state
},
}),
],
},
});
```
## Action Stubbing
### Execute Real Actions
```ts
createTestingPinia({ stubActions: false });
```
### Selective Stubbing
```ts
// Only stub specific actions
createTestingPinia({
stubActions: ['increment', 'reset'],
});
// Or use a function
createTestingPinia({
stubActions: (actionName, store) => {
if (actionName.startsWith('set')) return true;
return false;
},
});
```
### Mock Action Return Values
```ts
import type { Mock } from 'vitest';
// After getting store
store.someAction.mockResolvedValue('mocked value');
```
## Mocking Getters
Getters are writable in tests:
```ts
const pinia = createTestingPinia();
const counter = useCounterStore(pinia);
counter.double = 3; // Override computed value
// Reset to default behavior
counter.double = undefined;
counter.double; // Now computed normally
```
## Custom Spy Function
If not using Jest/Vitest with globals:
```ts
import { vi } from 'vitest';
createTestingPinia({
createSpy: vi.fn,
});
```
With Sinon:
```ts
import sinon from 'sinon';
createTestingPinia({
createSpy: sinon.spy,
});
```
## Pinia Plugins in Tests
Pass plugins to `createTestingPinia()`:
```ts
import { somePlugin } from '../src/stores/plugin';
createTestingPinia({
stubActions: false,
plugins: [somePlugin],
});
```
**Don't use** `testingPinia.use(MyPlugin)` - pass plugins in options.
## Type-Safe Mocked Store
```ts
import type { Mock } from 'vitest';
import type { Store, StoreDefinition } from 'pinia';
function mockedStore<TStoreDef extends () => unknown>(
useStore: TStoreDef,
): TStoreDef extends StoreDefinition<infer Id, infer State, infer Getters, infer Actions>
? Store<
Id,
State,
Record<string, never>,
{
[K in keyof Actions]: Actions[K] extends (...args: any[]) => any
? Mock<Actions[K]>
: Actions[K];
}
>
: ReturnType<TStoreDef> {
return useStore() as any;
}
// Usage
const store = mockedStore(useSomeStore);
store.someAction.mockResolvedValue('value'); // Typed!
```
## E2E Tests
No special handling needed - Pinia works normally.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/testing.html
-->
@@ -0,0 +1,389 @@
---
name: stores
description: Defining stores, state, getters, and actions in Pinia
---
# Pinia Stores
Stores are defined using `defineStore()` with a unique name. Each store has three core concepts: **state**, **getters**, and **actions**.
## Defining Stores
### Option Stores
Similar to Vue's Options API:
```ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo',
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});
```
Think of `state` as `data`, `getters` as `computed`, and `actions` as `methods`.
### Setup Stores (Recommended)
Uses Composition API syntax - more flexible and powerful:
```ts
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const name = ref('Eduardo');
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, name, doubleCount, increment };
});
```
In Setup Stores: `ref()` → state, `computed()` → getters, `function()` → actions.
**Important:** You must return all state properties for Pinia to track them.
### Using Stores
```vue
<script setup>
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore();
// Access: store.count, store.doubleCount, store.increment()
</script>
```
### Destructuring with storeToRefs
```vue
<script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore();
// ❌ Breaks reactivity
const { name, doubleCount } = store;
// ✅ Preserves reactivity for state/getters
const { name, doubleCount } = storeToRefs(store);
// ✅ Actions can be destructured directly
const { increment } = store;
</script>
```
---
## State
State is defined as a function returning the initial state.
### TypeScript
Type inference works automatically. For complex types:
```ts
interface UserInfo {
name: string;
age: number;
}
export const useUserStore = defineStore('user', {
state: () => ({
userList: [] as UserInfo[],
user: null as UserInfo | null,
}),
});
```
Or use an interface for the return type:
```ts
interface State {
userList: UserInfo[];
user: UserInfo | null;
}
export const useUserStore = defineStore('user', {
state: (): State => ({
userList: [],
user: null,
}),
});
```
### Accessing and Modifying
```ts
const store = useStore();
store.count++;
```
```vue
<input v-model="store.count" type="number" />
```
### Mutating with $patch
Apply multiple changes at once:
```ts
// Object syntax
store.$patch({
count: store.count + 1,
name: 'DIO',
});
// Function syntax (for complex mutations)
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 });
state.hasChanged = true;
});
```
### Resetting State
Option Stores have built-in `$reset()`. For Setup Stores, implement your own:
```ts
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
function $reset() {
count.value = 0;
}
return { count, $reset };
});
```
### Subscribing to State Changes
```ts
cartStore.$subscribe((mutation, state) => {
mutation.type; // 'direct' | 'patch object' | 'patch function'
mutation.storeId; // 'cart'
mutation.payload; // patch object (only for 'patch object')
localStorage.setItem('cart', JSON.stringify(state));
});
// Options
cartStore.$subscribe(callback, { flush: 'sync' }); // Immediate
cartStore.$subscribe(callback, { detached: true }); // Keep after unmount
```
---
## Getters
Getters are computed values, equivalent to Vue's `computed()`.
### Basic Getters
```ts
getters: {
doubleCount: (state) => state.count * 2,
}
```
### Accessing Other Getters
Use `this` with explicit return type:
```ts
getters: {
doubleCount: (state) => state.count * 2,
doublePlusOne(): number {
return this.doubleCount + 1
},
},
```
### Getters with Arguments
Return a function (note: loses caching):
```ts
getters: {
getUserById: (state) => {
return (userId: string) => state.users.find((user) => user.id === userId)
},
},
```
Cache within parameterized getters:
```ts
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId: string) => activeUsers.find((user) => user.id === userId)
},
},
```
### Accessing Other Stores in Getters
```ts
import { useOtherStore } from './other-store'
getters: {
combined(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
```
---
## Actions
Actions are methods for business logic. Unlike getters, they can be asynchronous.
### Defining Actions
```ts
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
```
### Async Actions
```ts
actions: {
async registerUser(login: string, password: string) {
try {
this.userData = await api.post({ login, password })
} catch (error) {
return error
}
},
},
```
### Accessing Other Stores in Actions
```ts
import { useAuthStore } from './auth-store'
actions: {
async fetchUserPreferences() {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
}
},
},
```
**SSR:** Call all `useStore()` before any `await`:
```ts
async orderCart() {
// ✅ Call stores before await
const user = useUserStore()
await apiOrderCart(user.token, this.items)
// ❌ Don't call useStore() after await in SSR
}
```
### Subscribing to Actions
```ts
const unsubscribe = someStore.$onAction(({ name, store, args, after, onError }) => {
const startTime = Date.now();
console.log(`Start "${name}" with params [${args.join(', ')}]`);
after((result) => {
console.log(`Finished "${name}" after ${Date.now() - startTime}ms`);
});
onError((error) => {
console.warn(`Failed "${name}": ${error}`);
});
});
unsubscribe(); // Cleanup
```
Keep subscription after component unmount:
```ts
someStore.$onAction(callback, true);
```
---
## Options API Helpers
```ts
import { mapState, mapWritableState, mapActions } from 'pinia';
import { useCounterStore } from '../stores/counter';
export default {
computed: {
// Readonly state/getters
...mapState(useCounterStore, ['count', 'doubleCount']),
// Writable state
...mapWritableState(useCounterStore, ['count']),
},
methods: {
...mapActions(useCounterStore, ['increment']),
},
};
```
---
## Accessing Global Providers in Setup Stores
```ts
import { inject } from 'vue';
import { useRoute } from 'vue-router';
import { defineStore } from 'pinia';
export const useSearchFilters = defineStore('search-filters', () => {
const route = useRoute();
const appProvided = inject('appProvided');
// Don't return these - access them directly in components
return {
/* ... */
};
});
```
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/
- https://pinia.vuejs.org/core-concepts/state.html
- https://pinia.vuejs.org/core-concepts/getters.html
- https://pinia.vuejs.org/core-concepts/actions.html
-->
@@ -0,0 +1,117 @@
---
name: composables-in-stores
description: Using Vue composables within Pinia stores
---
# Composables in Stores
Pinia stores can leverage Vue composables for reusable stateful logic.
## Option Stores
Call composables inside the `state` property, but only those returning writable refs:
```ts
import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core';
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
});
```
**Works:** Composables returning `ref()`:
- `useLocalStorage`
- `useAsyncState`
**Doesn't work in Option Stores:**
- Composables exposing functions
- Composables exposing readonly data
## Setup Stores
More flexible - can use almost any composable:
```ts
import { defineStore } from 'pinia';
import { useMediaControls } from '@vueuse/core';
import { ref } from 'vue';
export const useVideoPlayer = defineStore('video', () => {
const videoElement = ref<HTMLVideoElement>();
const src = ref('/data/video.mp4');
const { playing, volume, currentTime, togglePictureInPicture } = useMediaControls(videoElement, {
src,
});
function loadVideo(element: HTMLVideoElement, newSrc: string) {
videoElement.value = element;
src.value = newSrc;
}
return {
src,
playing,
volume,
currentTime,
loadVideo,
togglePictureInPicture,
};
});
```
**Note:** Don't return non-serializable DOM refs like `videoElement` - they're internal implementation details.
## SSR Considerations
### Option Stores with hydrate()
Define a `hydrate()` function to handle client-side hydration:
```ts
import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core';
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
hydrate(state, initialState) {
// Ignore server state, read from browser
state.user = useLocalStorage('pinia/auth/login', 'bob');
},
});
```
### Setup Stores with skipHydrate()
Mark state that shouldn't hydrate from server:
```ts
import { defineStore, skipHydrate } from 'pinia';
import { useEyeDropper, useLocalStorage } from '@vueuse/core';
export const useColorStore = defineStore('colors', () => {
const { isSupported, open, sRGBHex } = useEyeDropper();
const lastColor = useLocalStorage('lastColor', sRGBHex);
return {
// Skip hydration for client-only state
lastColor: skipHydrate(lastColor),
open, // Function - no hydration needed
isSupported, // Boolean - not reactive
};
});
```
`skipHydrate()` only applies to state properties (refs), not functions or non-reactive values.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/composables.html
-->
@@ -0,0 +1,134 @@
---
name: composing-stores
description: Store-to-store communication and avoiding circular dependencies
---
# Composing Stores
Stores can use each other for shared state and logic.
## Rule: Avoid Circular Dependencies
Two stores cannot directly read each other's state during setup:
```ts
// ❌ Infinite loop
const useX = defineStore('x', () => {
const y = useY();
y.name; // Don't read here!
return { name: ref('X') };
});
const useY = defineStore('y', () => {
const x = useX();
x.name; // Don't read here!
return { name: ref('Y') };
});
```
**Solution:** Read in getters, computed, or actions:
```ts
const useX = defineStore('x', () => {
const y = useY();
// ✅ Read in computed/actions
function doSomething() {
const yName = y.name;
}
return { name: ref('X'), doSomething };
});
```
## Setup Stores: Use Store at Top
```ts
import { defineStore } from 'pinia';
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', () => {
const user = useUserStore();
const list = ref([]);
const summary = computed(() => {
return `Hi ${user.name}, you have ${list.value.length} items`;
});
function purchase() {
return apiPurchase(user.id, list.value);
}
return { list, summary, purchase };
});
```
## Shared Getters
Call `useStore()` inside a getter:
```ts
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', {
getters: {
summary(state) {
const user = useUserStore();
return `Hi ${user.name}, you have ${state.list.length} items`;
},
},
});
```
## Shared Actions
Call `useStore()` inside an action:
```ts
import { useUserStore } from './user';
import { apiOrderCart } from './api';
export const useCartStore = defineStore('cart', {
actions: {
async orderCart() {
const user = useUserStore();
try {
await apiOrderCart(user.token, this.items);
this.emptyCart();
} catch (err) {
displayError(err);
}
},
},
});
```
## SSR: Call Stores Before Await
In async actions, call all stores before any `await`:
```ts
actions: {
async orderCart() {
// ✅ All useStore() calls before await
const user = useUserStore()
const analytics = useAnalyticsStore()
try {
await apiOrderCart(user.token, this.items)
// ❌ Don't call useStore() after await (SSR issue)
// const otherStore = useOtherStore()
} catch (err) {
displayError(err)
}
},
}
```
This ensures the correct Pinia instance is used during SSR.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/composing-stores.html
-->
@@ -0,0 +1,207 @@
---
name: plugins
description: Extend stores with custom properties, methods, and behavior
---
# Plugins
Plugins extend all stores with custom properties, methods, or behavior.
## Basic Plugin
```ts
import { createPinia } from 'pinia';
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' };
}
const pinia = createPinia();
pinia.use(SecretPiniaPlugin);
// In any store
const store = useStore();
store.secret; // 'the cake is a lie'
```
## Plugin Context
Plugins receive a context object:
```ts
import { PiniaPluginContext } from 'pinia';
export function myPiniaPlugin(context: PiniaPluginContext) {
context.pinia; // pinia instance
context.app; // Vue app instance
context.store; // store being augmented
context.options; // store definition options
}
```
## Adding Properties
Return an object to add properties (tracked in devtools):
```ts
pinia.use(() => ({ hello: 'world' }));
```
Or set directly on store:
```ts
pinia.use(({ store }) => {
store.hello = 'world';
// For devtools visibility in dev mode
if (process.env.NODE_ENV === 'development') {
store._customProperties.add('hello');
}
});
```
## Adding State
Add to both `store` and `store.$state` for SSR/devtools:
```ts
import { toRef, ref } from 'vue';
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty('hasError')) {
const hasError = ref(false);
store.$state.hasError = hasError;
}
store.hasError = toRef(store.$state, 'hasError');
});
```
## Adding External Properties
Wrap non-reactive objects with `markRaw()`:
```ts
import { markRaw } from 'vue';
import { router } from './router';
pinia.use(({ store }) => {
store.router = markRaw(router);
});
```
## Custom Store Options
Define custom options consumed by plugins:
```ts
// Store definition
defineStore('search', {
actions: {
searchContacts() {
/* ... */
},
},
debounce: {
searchContacts: 300,
},
});
// Plugin reads custom option
import debounce from 'lodash/debounce';
pinia.use(({ options, store }) => {
if (options.debounce) {
return Object.keys(options.debounce).reduce((acc, action) => {
acc[action] = debounce(store[action], options.debounce[action]);
return acc;
}, {});
}
});
```
For Setup Stores, pass options as third argument:
```ts
defineStore(
'search',
() => {
/* ... */
},
{
debounce: { searchContacts: 300 },
},
);
```
## TypeScript Augmentation
### Custom Properties
```ts
import 'pinia';
import type { Router } from 'vue-router';
declare module 'pinia' {
export interface PiniaCustomProperties {
router: Router;
hello: string;
}
}
```
### Custom State
```ts
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hasError: boolean;
}
}
```
### Custom Options
```ts
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
debounce?: Partial<Record<keyof StoreActions<Store>, number>>;
}
}
```
## Subscribe in Plugins
```ts
pinia.use(({ store }) => {
store.$subscribe(() => {
// React to state changes
});
store.$onAction(() => {
// React to actions
});
});
```
## Nuxt Plugin
Create a Nuxt plugin to add Pinia plugins:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia';
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`);
});
return { creationTime: new Date() };
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin);
});
```
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/plugins.html
-->