feat: Complete fleet — 94 skills across 10+ domains

Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-16 16:27:42 -06:00
parent 861b28b965
commit f5792c40be
1262 changed files with 212048 additions and 61 deletions

View File

@@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/pinia`
- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e`
- **Generated:** 2026-01-28

59
skills/pinia/SKILL.md Normal file
View File

@@ -0,0 +1,59 @@
---
name: pinia
description: Pinia official Vue state management library, type-safe and extensible. Use when defining stores, working with state/getters/actions, or implementing store patterns in Vue apps.
metadata:
author: Anthony Fu
version: "2026.1.28"
source: Generated from https://github.com/vuejs/pinia, scripts located at https://github.com/antfu/skills
---
# Pinia
Pinia is the official state management library for Vue, designed to be intuitive and type-safe. It supports both Options API and Composition API styles, with first-class TypeScript support and devtools integration.
> The skill is based on Pinia v3.0.4, generated at 2026-01-28.
## Core References
| Topic | Description | Reference |
|-------|-------------|-----------|
| Stores | Defining stores, state, getters, actions, storeToRefs, subscriptions | [core-stores](references/core-stores.md) |
## Features
### Extensibility
| Topic | Description | Reference |
|-------|-------------|-----------|
| Plugins | Extend stores with custom properties, state, and behavior | [features-plugins](references/features-plugins.md) |
### Composability
| Topic | Description | Reference |
|-------|-------------|-----------|
| Composables | Using Vue composables within stores (VueUse, etc.) | [features-composables](references/features-composables.md) |
| Composing Stores | Store-to-store communication, avoiding circular dependencies | [features-composing-stores](references/features-composing-stores.md) |
## Best Practices
| Topic | Description | Reference |
|-------|-------------|-----------|
| Testing | Unit testing with @pinia/testing, mocking, stubbing | [best-practices-testing](references/best-practices-testing.md) |
| Outside Components | Using stores in navigation guards, plugins, middlewares | [best-practices-outside-component](references/best-practices-outside-component.md) |
## Advanced
| Topic | Description | Reference |
|-------|-------------|-----------|
| SSR | Server-side rendering, state hydration | [advanced-ssr](references/advanced-ssr.md) |
| Nuxt | Nuxt integration, auto-imports, SSR best practices | [advanced-nuxt](references/advanced-nuxt.md) |
| HMR | Hot module replacement for development | [advanced-hmr](references/advanced-hmr.md) |
## Key Recommendations
- **Prefer Setup Stores** for complex logic, composables, and watchers
- **Use `storeToRefs()`** when destructuring state/getters to preserve reactivity
- **Actions can be destructured directly** - they're bound to the store
- **Call stores inside functions** not at module scope, especially for SSR
- **Add HMR support** to each store for better development experience
- **Use `@pinia/testing`** for component tests with mocked stores

View File

@@ -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
-->

View File

@@ -0,0 +1,119 @@
---
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
-->

View File

@@ -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/
-->

View File

@@ -0,0 +1,115 @@
---
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
-->

View File

@@ -0,0 +1,212 @@
---
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
-->

View File

@@ -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
-->

View File

@@ -0,0 +1,114 @@
---
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
-->

View File

@@ -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
-->

View File

@@ -0,0 +1,203 @@
---
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
-->