format: apply repo prettier (3.8.1) to the folded skills tree

963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
This commit is contained in:
fargo
2026-08-19 14:37:17 -05:00
parent d2eeb64433
commit 1a822493ba
962 changed files with 29594 additions and 27188 deletions
+16 -16
View File
@@ -3,7 +3,7 @@ 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"
version: '2026.1.28'
source: Generated from https://github.com/vuejs/pinia, scripts located at https://github.com/antfu/skills
---
@@ -15,39 +15,39 @@ Pinia is the official state management library for Vue, designed to be intuitive
## Core References
| Topic | Description | Reference |
|-------|-------------|-----------|
| Topic | Description | Reference |
| ------ | -------------------------------------------------------------------- | ---------------------------------------- |
| Stores | Defining stores, state, getters, actions, storeToRefs, subscriptions | [core-stores](references/core-stores.md) |
## Features
### Extensibility
| Topic | Description | Reference |
|-------|-------------|-----------|
| 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) |
| 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) |
| 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) |
| 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
@@ -12,30 +12,30 @@ Pinia supports HMR to edit stores without page reload, preserving existing state
Add this snippet after each store definition:
```ts
import { defineStore, acceptHMRUpdate } from 'pinia'
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))
import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot));
}
```
## Setup Store Example
```ts
import { defineStore, acceptHMRUpdate } from 'pinia'
import { defineStore, acceptHMRUpdate } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
})
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot));
}
```
@@ -16,6 +16,7 @@ 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" }
> ```
@@ -26,12 +27,13 @@ This installs both `@pinia/nuxt` and `pinia`. If `pinia` isn't installed, add it
// 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
@@ -48,7 +50,7 @@ export default defineNuxtConfig({
pinia: {
storesDirs: ['./stores/**', './custom-folder/stores/**'],
},
})
});
```
## Fetching Data in Pages
@@ -57,10 +59,10 @@ Use `callOnce()` for SSR-friendly data fetching:
```vue
<script setup>
const store = useStore()
const store = useStore();
// Run once, data persists across navigations
await callOnce('user', () => store.fetchUser())
await callOnce('user', () => store.fetchUser());
</script>
```
@@ -68,10 +70,10 @@ await callOnce('user', () => store.fetchUser())
```vue
<script setup>
const store = useStore()
const store = useStore();
// Refetch on every navigation (like useFetch)
await callOnce('user', () => store.fetchUser(), { mode: 'navigation' })
await callOnce('user', () => store.fetchUser(), { mode: 'navigation' });
</script>
```
@@ -82,13 +84,13 @@ 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)
const nuxtApp = useNuxtApp();
const store = useStore(nuxtApp.$pinia);
if (to.meta.requiresAuth && !store.isLoggedIn) {
return navigateTo('/login')
return navigateTo('/login');
}
})
});
```
Most of the time, you don't need this - just use stores in components or other injection-aware contexts.
@@ -99,18 +101,18 @@ Create a Nuxt plugin:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { PiniaPluginContext } from 'pinia';
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`)
})
return { creationTime: new Date() }
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`);
});
return { creationTime: new Date() };
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})
$pinia.use(MyPiniaPlugin);
});
```
<!--
@@ -14,7 +14,7 @@ Pinia works with SSR when stores are called at the top of `setup`, getters, or a
```vue
<script setup>
// ✅ Works - pinia knows the app context in setup
const main = useMainStore()
const main = useMainStore();
</script>
```
@@ -23,19 +23,19 @@ const main = useMainStore()
Pass the `pinia` instance explicitly:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
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)
const main = useMainStore(pinia);
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login'
return '/login';
}
})
});
```
## serverPrefetch()
@@ -45,10 +45,10 @@ Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
return store.fetchData()
const store = useStore(this.$pinia);
return store.fetchData();
},
}
};
```
## onServerPrefetch()
@@ -57,11 +57,11 @@ Works normally:
```vue
<script setup>
const store = useStore()
const store = useStore();
onServerPrefetch(async () => {
await store.fetchData()
})
await store.fetchData();
});
</script>
```
@@ -74,16 +74,16 @@ Serialize state on server and hydrate on client.
Use [devalue](https://github.com/Rich-Harris/devalue) for XSS-safe serialization:
```ts
import devalue from 'devalue'
import { createPinia } from 'pinia'
import devalue from 'devalue';
import { createPinia } from 'pinia';
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(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)
const serializedState = devalue(pinia.state.value);
// Inject into HTML as global variable
```
@@ -92,13 +92,13 @@ const serializedState = devalue(pinia.state.value)
Hydrate before any `useStore()` call:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
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)
pinia.state.value = JSON.parse(window.__pinia);
}
```
@@ -12,20 +12,20 @@ Stores need the `pinia` instance, which is automatically injected in components.
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'
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 userStore = useUserStore();
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
// ✅ Works - pinia is active
const userStore = useUserStore()
const userStore = useUserStore();
```
## Navigation Guards
@@ -33,15 +33,19 @@ const userStore = useUserStore()
**Wrong:** Call at module level
```ts
import { createRouter } from 'vue-router'
const router = createRouter({ /* ... */ })
import { createRouter } from 'vue-router';
const router = createRouter({
/* ... */
});
// ❌ May fail depending on import order
const store = useUserStore()
const store = useUserStore();
router.beforeEach((to) => {
if (store.isLoggedIn) { /* ... */ }
})
if (store.isLoggedIn) {
/* ... */
}
});
```
**Correct:** Call inside the guard
@@ -49,12 +53,12 @@ router.beforeEach((to) => {
```ts
router.beforeEach((to) => {
// ✅ Called after pinia is installed
const store = useUserStore()
const store = useUserStore();
if (to.meta.requiresAuth && !store.isLoggedIn) {
return '/login'
return '/login';
}
})
});
```
## SSR Applications
@@ -62,19 +66,19 @@ router.beforeEach((to) => {
Always pass the `pinia` instance to `useStore()`:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
const pinia = createPinia();
const app = createApp(App);
app.use(router);
app.use(pinia);
router.beforeEach((to) => {
// ✅ Pass pinia instance
const main = useMainStore(pinia)
const main = useMainStore(pinia);
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login'
return '/login';
}
})
});
```
## serverPrefetch()
@@ -84,10 +88,10 @@ Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
return store.fetchData()
const store = useStore(this.$pinia);
return store.fetchData();
},
}
};
```
## onServerPrefetch()
@@ -96,12 +100,12 @@ Works normally in `<script setup>`:
```vue
<script setup>
const store = useStore()
const store = useStore();
onServerPrefetch(async () => {
// ✅ Just works
await store.fetchData()
})
await store.fetchData();
});
</script>
```
@@ -10,37 +10,37 @@ description: Unit testing stores and components with @pinia/testing
Create a fresh pinia instance for each test:
```ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'
import { setActivePinia, createPinia } from 'pinia';
import { useCounterStore } from '../src/stores/counter';
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
setActivePinia(createPinia());
});
it('increments', () => {
const counter = useCounterStore()
expect(counter.n).toBe(0)
counter.increment()
expect(counter.n).toBe(1)
})
})
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'
import { setActivePinia, createPinia } from 'pinia';
import { createApp } from 'vue';
import { somePlugin } from '../src/stores/plugin';
const app = createApp({})
const app = createApp({});
beforeEach(() => {
const pinia = createPinia().use(somePlugin)
app.use(pinia)
setActivePinia(pinia)
})
const pinia = createPinia().use(somePlugin);
app.use(pinia);
setActivePinia(pinia);
});
```
## Testing Components
@@ -54,25 +54,25 @@ npm i -D @pinia/testing
Use `createTestingPinia()`:
```ts
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { useSomeStore } from '@/stores/myStore'
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()
const store = useSomeStore();
// Manipulate state directly
store.name = 'new name'
store.$patch({ name: 'new name' })
store.name = 'new name';
store.$patch({ name: 'new name' });
// Actions are stubbed by default
store.someAction()
expect(store.someAction).toHaveBeenCalledTimes(1)
store.someAction();
expect(store.someAction).toHaveBeenCalledTimes(1);
```
## Initial State
@@ -90,7 +90,7 @@ const wrapper = mount(Counter, {
}),
],
},
})
});
```
## Action Stubbing
@@ -98,7 +98,7 @@ const wrapper = mount(Counter, {
### Execute Real Actions
```ts
createTestingPinia({ stubActions: false })
createTestingPinia({ stubActions: false });
```
### Selective Stubbing
@@ -107,24 +107,24 @@ createTestingPinia({ stubActions: false })
// Only stub specific actions
createTestingPinia({
stubActions: ['increment', 'reset'],
})
});
// Or use a function
createTestingPinia({
stubActions: (actionName, store) => {
if (actionName.startsWith('set')) return true
return false
if (actionName.startsWith('set')) return true;
return false;
},
})
});
```
### Mock Action Return Values
```ts
import type { Mock } from 'vitest'
import type { Mock } from 'vitest';
// After getting store
store.someAction.mockResolvedValue('mocked value')
store.someAction.mockResolvedValue('mocked value');
```
## Mocking Getters
@@ -132,14 +132,14 @@ store.someAction.mockResolvedValue('mocked value')
Getters are writable in tests:
```ts
const pinia = createTestingPinia()
const counter = useCounterStore(pinia)
const pinia = createTestingPinia();
const counter = useCounterStore(pinia);
counter.double = 3 // Override computed value
counter.double = 3; // Override computed value
// Reset to default behavior
counter.double = undefined
counter.double // Now computed normally
counter.double = undefined;
counter.double; // Now computed normally
```
## Custom Spy Function
@@ -147,21 +147,21 @@ counter.double // Now computed normally
If not using Jest/Vitest with globals:
```ts
import { vi } from 'vitest'
import { vi } from 'vitest';
createTestingPinia({
createSpy: vi.fn,
})
});
```
With Sinon:
```ts
import sinon from 'sinon'
import sinon from 'sinon';
createTestingPinia({
createSpy: sinon.spy,
})
});
```
## Pinia Plugins in Tests
@@ -169,12 +169,12 @@ createTestingPinia({
Pass plugins to `createTestingPinia()`:
```ts
import { somePlugin } from '../src/stores/plugin'
import { somePlugin } from '../src/stores/plugin';
createTestingPinia({
stubActions: false,
plugins: [somePlugin],
})
});
```
**Don't use** `testingPinia.use(MyPlugin)` - pass plugins in options.
@@ -182,24 +182,29 @@ createTestingPinia({
## Type-Safe Mocked Store
```ts
import type { Mock } from 'vitest'
import type { Store, StoreDefinition } from 'pinia'
import type { Mock } from 'vitest';
import type { Store, StoreDefinition } from 'pinia';
function mockedStore<TStoreDef extends () => unknown>(
useStore: TStoreDef
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]
}>
? 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
return useStore() as any;
}
// Usage
const store = mockedStore(useSomeStore)
store.someAction.mockResolvedValue('value') // Typed!
const store = mockedStore(useSomeStore);
store.someAction.mockResolvedValue('value'); // Typed!
```
## E2E Tests
@@ -14,7 +14,7 @@ Stores are defined using `defineStore()` with a unique name. Each store has thre
Similar to Vue's Options API:
```ts
import { defineStore } from 'pinia'
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
@@ -26,10 +26,10 @@ export const useCounterStore = defineStore('counter', {
},
actions: {
increment() {
this.count++
this.count++;
},
},
})
});
```
Think of `state` as `data`, `getters` as `computed`, and `actions` as `methods`.
@@ -39,20 +39,20 @@ Think of `state` as `data`, `getters` as `computed`, and `actions` as `methods`.
Uses Composition API syntax - more flexible and powerful:
```ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
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)
const count = ref(0);
const name = ref('Eduardo');
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++
count.value++;
}
return { count, name, doubleCount, increment }
})
return { count, name, doubleCount, increment };
});
```
In Setup Stores: `ref()` → state, `computed()` → getters, `function()` → actions.
@@ -63,9 +63,9 @@ In Setup Stores: `ref()` → state, `computed()` → getters, `function()` → a
```vue
<script setup>
import { useCounterStore } from '@/stores/counter'
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore()
const store = useCounterStore();
// Access: store.count, store.doubleCount, store.increment()
</script>
```
@@ -74,19 +74,19 @@ const store = useCounterStore()
```vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore()
const store = useCounterStore();
// ❌ Breaks reactivity
const { name, doubleCount } = store
const { name, doubleCount } = store;
// ✅ Preserves reactivity for state/getters
const { name, doubleCount } = storeToRefs(store)
const { name, doubleCount } = storeToRefs(store);
// ✅ Actions can be destructured directly
const { increment } = store
const { increment } = store;
</script>
```
@@ -102,8 +102,8 @@ Type inference works automatically. For complex types:
```ts
interface UserInfo {
name: string
age: number
name: string;
age: number;
}
export const useUserStore = defineStore('user', {
@@ -111,15 +111,15 @@ export const useUserStore = defineStore('user', {
userList: [] as UserInfo[],
user: null as UserInfo | null,
}),
})
});
```
Or use an interface for the return type:
```ts
interface State {
userList: UserInfo[]
user: UserInfo | null
userList: UserInfo[];
user: UserInfo | null;
}
export const useUserStore = defineStore('user', {
@@ -127,14 +127,14 @@ export const useUserStore = defineStore('user', {
userList: [],
user: null,
}),
})
});
```
### Accessing and Modifying
```ts
const store = useStore()
store.count++
const store = useStore();
store.count++;
```
```vue
@@ -150,13 +150,13 @@ Apply multiple changes at once:
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
})
state.items.push({ name: 'shoes', quantity: 1 });
state.hasChanged = true;
});
```
### Resetting State
@@ -165,30 +165,30 @@ Option Stores have built-in `$reset()`. For Setup Stores, implement your own:
```ts
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const count = ref(0);
function $reset() {
count.value = 0
count.value = 0;
}
return { count, $reset }
})
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')
mutation.type; // 'direct' | 'patch object' | 'patch function'
mutation.storeId; // 'cart'
mutation.payload; // patch object (only for 'patch object')
localStorage.setItem('cart', JSON.stringify(state))
})
localStorage.setItem('cart', JSON.stringify(state));
});
// Options
cartStore.$subscribe(callback, { flush: 'sync' }) // Immediate
cartStore.$subscribe(callback, { detached: true }) // Keep after unmount
cartStore.$subscribe(callback, { flush: 'sync' }); // Immediate
cartStore.$subscribe(callback, { detached: true }); // Keep after unmount
```
---
@@ -317,28 +317,26 @@ async orderCart() {
### 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(', ')}]`)
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`)
})
after((result) => {
console.log(`Finished "${name}" after ${Date.now() - startTime}ms`);
});
onError((error) => {
console.warn(`Failed "${name}": ${error}`)
})
}
)
onError((error) => {
console.warn(`Failed "${name}": ${error}`);
});
});
unsubscribe() // Cleanup
unsubscribe(); // Cleanup
```
Keep subscription after component unmount:
```ts
someStore.$onAction(callback, true)
someStore.$onAction(callback, true);
```
---
@@ -346,8 +344,8 @@ someStore.$onAction(callback, true)
## Options API Helpers
```ts
import { mapState, mapWritableState, mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'
import { mapState, mapWritableState, mapActions } from 'pinia';
import { useCounterStore } from '../stores/counter';
export default {
computed: {
@@ -359,7 +357,7 @@ export default {
methods: {
...mapActions(useCounterStore, ['increment']),
},
}
};
```
---
@@ -367,17 +365,19 @@ export default {
## Accessing Global Providers in Setup Stores
```ts
import { inject } from 'vue'
import { useRoute } from 'vue-router'
import { defineStore } from 'pinia'
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')
const route = useRoute();
const appProvided = inject('appProvided');
// Don't return these - access them directly in components
return { /* ... */ }
})
return {
/* ... */
};
});
```
<!--
@@ -12,21 +12,23 @@ Pinia stores can leverage Vue composables for reusable stateful logic.
Call composables inside the `state` property, but only those returning writable refs:
```ts
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
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
@@ -35,19 +37,20 @@ export const useAuthStore = defineStore('auth', {
More flexible - can use almost any composable:
```ts
import { defineStore } from 'pinia'
import { useMediaControls } from '@vueuse/core'
import { ref } from 'vue'
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 })
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
videoElement.value = element;
src.value = newSrc;
}
return {
@@ -57,8 +60,8 @@ export const useVideoPlayer = defineStore('video', () => {
currentTime,
loadVideo,
togglePictureInPicture,
}
})
};
});
```
**Note:** Don't return non-serializable DOM refs like `videoElement` - they're internal implementation details.
@@ -70,8 +73,8 @@ export const useVideoPlayer = defineStore('video', () => {
Define a `hydrate()` function to handle client-side hydration:
```ts
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core';
export const useAuthStore = defineStore('auth', {
state: () => ({
@@ -80,9 +83,9 @@ export const useAuthStore = defineStore('auth', {
hydrate(state, initialState) {
// Ignore server state, read from browser
state.user = useLocalStorage('pinia/auth/login', 'bob')
state.user = useLocalStorage('pinia/auth/login', 'bob');
},
})
});
```
### Setup Stores with skipHydrate()
@@ -90,20 +93,20 @@ export const useAuthStore = defineStore('auth', {
Mark state that shouldn't hydrate from server:
```ts
import { defineStore, skipHydrate } from 'pinia'
import { useEyeDropper, useLocalStorage } from '@vueuse/core'
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)
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
open, // Function - no hydration needed
isSupported, // Boolean - not reactive
}
})
};
});
```
`skipHydrate()` only applies to state properties (refs), not functions or non-reactive values.
@@ -14,53 +14,53 @@ 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 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') }
})
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()
const y = useY();
// ✅ Read in computed/actions
function doSomething() {
const yName = y.name
const yName = y.name;
}
return { name: ref('X'), doSomething }
})
return { name: ref('X'), doSomething };
});
```
## Setup Stores: Use Store at Top
```ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { defineStore } from 'pinia';
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', () => {
const user = useUserStore()
const list = ref([])
const user = useUserStore();
const list = ref([]);
const summary = computed(() => {
return `Hi ${user.name}, you have ${list.value.length} items`
})
return `Hi ${user.name}, you have ${list.value.length} items`;
});
function purchase() {
return apiPurchase(user.id, list.value)
return apiPurchase(user.id, list.value);
}
return { list, summary, purchase }
})
return { list, summary, purchase };
});
```
## Shared Getters
@@ -68,16 +68,16 @@ export const useCartStore = defineStore('cart', () => {
Call `useStore()` inside a getter:
```ts
import { useUserStore } from './user'
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`
const user = useUserStore();
return `Hi ${user.name}, you have ${state.list.length} items`;
},
},
})
});
```
## Shared Actions
@@ -85,23 +85,23 @@ export const useCartStore = defineStore('cart', {
Call `useStore()` inside an action:
```ts
import { useUserStore } from './user'
import { apiOrderCart } from './api'
import { useUserStore } from './user';
import { apiOrderCart } from './api';
export const useCartStore = defineStore('cart', {
actions: {
async orderCart() {
const user = useUserStore()
const user = useUserStore();
try {
await apiOrderCart(user.token, this.items)
this.emptyCart()
await apiOrderCart(user.token, this.items);
this.emptyCart();
} catch (err) {
displayError(err)
displayError(err);
}
},
},
})
});
```
## SSR: Call Stores Before Await
@@ -10,18 +10,18 @@ Plugins extend all stores with custom properties, methods, or behavior.
## Basic Plugin
```ts
import { createPinia } from 'pinia'
import { createPinia } from 'pinia';
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
return { secret: 'the cake is a lie' };
}
const pinia = createPinia()
pinia.use(SecretPiniaPlugin)
const pinia = createPinia();
pinia.use(SecretPiniaPlugin);
// In any store
const store = useStore()
store.secret // 'the cake is a lie'
const store = useStore();
store.secret; // 'the cake is a lie'
```
## Plugin Context
@@ -29,13 +29,13 @@ store.secret // 'the cake is a lie'
Plugins receive a context object:
```ts
import { PiniaPluginContext } from 'pinia'
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
context.pinia; // pinia instance
context.app; // Vue app instance
context.store; // store being augmented
context.options; // store definition options
}
```
@@ -44,19 +44,19 @@ export function myPiniaPlugin(context: PiniaPluginContext) {
Return an object to add properties (tracked in devtools):
```ts
pinia.use(() => ({ hello: 'world' }))
pinia.use(() => ({ hello: 'world' }));
```
Or set directly on store:
```ts
pinia.use(({ store }) => {
store.hello = 'world'
store.hello = 'world';
// For devtools visibility in dev mode
if (process.env.NODE_ENV === 'development') {
store._customProperties.add('hello')
store._customProperties.add('hello');
}
})
});
```
## Adding State
@@ -64,15 +64,15 @@ pinia.use(({ store }) => {
Add to both `store` and `store.$state` for SSR/devtools:
```ts
import { toRef, ref } from 'vue'
import { toRef, ref } from 'vue';
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty('hasError')) {
const hasError = ref(false)
store.$state.hasError = hasError
const hasError = ref(false);
store.$state.hasError = hasError;
}
store.hasError = toRef(store.$state, 'hasError')
})
store.hasError = toRef(store.$state, 'hasError');
});
```
## Adding External Properties
@@ -80,12 +80,12 @@ pinia.use(({ store }) => {
Wrap non-reactive objects with `markRaw()`:
```ts
import { markRaw } from 'vue'
import { router } from './router'
import { markRaw } from 'vue';
import { router } from './router';
pinia.use(({ store }) => {
store.router = markRaw(router)
})
store.router = markRaw(router);
});
```
## Custom Store Options
@@ -96,24 +96,26 @@ Define custom options consumed by plugins:
// Store definition
defineStore('search', {
actions: {
searchContacts() { /* ... */ },
searchContacts() {
/* ... */
},
},
debounce: {
searchContacts: 300,
},
})
});
// Plugin reads custom option
import debounce from 'lodash/debounce'
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
}, {})
acc[action] = debounce(store[action], options.debounce[action]);
return acc;
}, {});
}
})
});
```
For Setup Stores, pass options as third argument:
@@ -121,11 +123,13 @@ For Setup Stores, pass options as third argument:
```ts
defineStore(
'search',
() => { /* ... */ },
() => {
/* ... */
},
{
debounce: { searchContacts: 300 },
}
)
},
);
```
## TypeScript Augmentation
@@ -133,13 +137,13 @@ defineStore(
### Custom Properties
```ts
import 'pinia'
import type { Router } from 'vue-router'
import 'pinia';
import type { Router } from 'vue-router';
declare module 'pinia' {
export interface PiniaCustomProperties {
router: Router
hello: string
router: Router;
hello: string;
}
}
```
@@ -149,7 +153,7 @@ declare module 'pinia' {
```ts
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hasError: boolean
hasError: boolean;
}
}
```
@@ -159,7 +163,7 @@ declare module 'pinia' {
```ts
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
debounce?: Partial<Record<keyof StoreActions<Store>, number>>;
}
}
```
@@ -170,11 +174,11 @@ declare module 'pinia' {
pinia.use(({ store }) => {
store.$subscribe(() => {
// React to state changes
})
});
store.$onAction(() => {
// React to actions
})
})
});
});
```
## Nuxt Plugin
@@ -183,18 +187,18 @@ Create a Nuxt plugin to add Pinia plugins:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { PiniaPluginContext } from 'pinia';
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`)
})
return { creationTime: new Date() }
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`);
});
return { creationTime: new Date() };
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})
$pinia.use(MyPiniaPlugin);
});
```
<!--