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:
5
skills/pinia/GENERATION.md
Normal file
5
skills/pinia/GENERATION.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Generation Info
|
||||
|
||||
- **Source:** `sources/pinia`
|
||||
- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e`
|
||||
- **Generated:** 2026-01-28
|
||||
59
skills/pinia/SKILL.md
Normal file
59
skills/pinia/SKILL.md
Normal 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
|
||||
61
skills/pinia/references/advanced-hmr.md
Normal file
61
skills/pinia/references/advanced-hmr.md
Normal 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
|
||||
-->
|
||||
119
skills/pinia/references/advanced-nuxt.md
Normal file
119
skills/pinia/references/advanced-nuxt.md
Normal 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
|
||||
-->
|
||||
121
skills/pinia/references/advanced-ssr.md
Normal file
121
skills/pinia/references/advanced-ssr.md
Normal 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/
|
||||
-->
|
||||
115
skills/pinia/references/best-practices-outside-component.md
Normal file
115
skills/pinia/references/best-practices-outside-component.md
Normal 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
|
||||
-->
|
||||
212
skills/pinia/references/best-practices-testing.md
Normal file
212
skills/pinia/references/best-practices-testing.md
Normal 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
|
||||
-->
|
||||
389
skills/pinia/references/core-stores.md
Normal file
389
skills/pinia/references/core-stores.md
Normal 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
|
||||
-->
|
||||
114
skills/pinia/references/features-composables.md
Normal file
114
skills/pinia/references/features-composables.md
Normal 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
|
||||
-->
|
||||
134
skills/pinia/references/features-composing-stores.md
Normal file
134
skills/pinia/references/features-composing-stores.md
Normal 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
|
||||
-->
|
||||
203
skills/pinia/references/features-plugins.md
Normal file
203
skills/pinia/references/features-plugins.md
Normal 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
|
||||
-->
|
||||
Reference in New Issue
Block a user