返回 Skills
antfu/skills· MIT 内容可用

pinia

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.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


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

TopicDescriptionReference
StoresDefining stores, state, getters, actions, storeToRefs, subscriptionscore-stores

Features

Extensibility

TopicDescriptionReference
PluginsExtend stores with custom properties, state, and behaviorfeatures-plugins

Composability

TopicDescriptionReference
ComposablesUsing Vue composables within stores (VueUse, etc.)features-composables
Composing StoresStore-to-store communication, avoiding circular dependenciesfeatures-composing-stores

Best Practices

TopicDescriptionReference
TestingUnit testing with @pinia/testing, mocking, stubbingbest-practices-testing
Outside ComponentsUsing stores in navigation guards, plugins, middlewaresbest-practices-outside-component

Advanced

TopicDescriptionReference
SSRServer-side rendering, state hydrationadvanced-ssr
NuxtNuxt integration, auto-imports, SSR best practicesadvanced-nuxt
HMRHot module replacement for developmentadvanced-hmr

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

附带文件

GENERATION.md
# Generation Info

- **Source:** `sources/pinia`
- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e`
- **Generated:** 2026-01-28
references/advanced-hmr.md
---
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
-->
references/advanced-nuxt.md
---
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
-->
references/advanced-ssr.md
---
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/
-->
references/best-practices-outside-component.md
---
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
-->
references/best-practices-testing.md
---
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
-->
references/core-stores.md
---
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
-->
references/features-composables.md
---
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
-->
references/features-composing-stores.md
---
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
-->
references/features-plugins.md
---
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
-->