**Status**: Production Ready ✅
/plugin marketplace add secondsky/claude-skills/plugin install pinia-v3@claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/example-template.txtreferences/example-reference.mdreferences/plugins-composables.mdreferences/ssr-and-nuxt.mdreferences/state-getters-actions.mdreferences/store-syntax-guide.mdreferences/testing-guide.mdreferences/vuex-migration-checklist.mdreferences/vuex-migration.mdscripts/api-store-example.tsscripts/auth-store-example.tsscripts/example-script.shscripts/option-store-template.tsscripts/persistence-plugin.tsscripts/setup-store-template.tsStatus: Production Ready ✅ Last Updated: 2025-11-11 Dependencies: Vue 3 (or Vue 2.7 with @vue/composition-api) Latest Versions: pinia@^3.0.4, @pinia/nuxt@^0.11.2, @pinia/testing@^1.0.2
bun add pinia
# or
bun add pinia
# or
bun add pinia
For Vue <2.7 users: Also install @vue/composition-api with bun add @vue/composition-api
Why this matters:
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
CRITICAL:
app.use(pinia) before mounting the app// stores/counter.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++
}
}
})
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
</div>
</template>
Load references/store-syntax-guide.md for complete comparison of Option vs Setup stores.
Pinia supports two store definition syntaxes:
Option Stores:
$reset() methodSetup Stores:
→ Load references/store-syntax-guide.md for: Complete syntax comparison, examples, choosing criteria
Load references/state-getters-actions.md for complete API reference.
State:
state: () => ({...}) (option) or ref() (setup)store.countstore.count++ or store.$patch({...})store.$reset() (option stores only)Getters:
getters: { double: (state) => state.count * 2 }this (must type return value)Actions:
actions: { increment() { this.count++ } }Store Destructuring:
import { storeToRefs } from 'pinia'
// ✅ For reactivity
const { name, count } = storeToRefs(store)
// ✅ Actions can destructure directly
const { increment } = store
→ Load references/state-getters-actions.md for: Complete API, subscriptions, store composition patterns, Options API usage
Load references/plugins-composables.md for complete plugin and composables guide.
pinia.use(({ store, options }) => {
// Add properties to every store
return { customProperty: 'value' }
})
Option Stores: Limited to useLocalStorage style in state()
Setup Stores: Full VueUse/composables support
→ Load references/plugins-composables.md for: Complete plugin patterns, VueUse integration, TypeScript typing, common patterns (persistence, router, logger)
Stores need the Pinia instance, which is auto-injected in components but not available in module scope.
// router.ts
import { useUserStore } from '@/stores/user'
// ❌ Fails: Pinia not installed yet
const userStore = useUserStore()
router.beforeEach((to) => {
if (userStore.isLoggedIn) { /* ... */ }
})
// router.ts
import { useUserStore } from '@/stores/user'
router.beforeEach((to) => {
// ✅ Works: Called after Pinia is installed
const userStore = useUserStore()
if (userStore.isLoggedIn) { /* ... */ }
})
Why it works: Router guards execute AFTER app.use(pinia) completes.
// server-side
export function setupRouter(pinia) {
router.beforeEach((to) => {
const userStore = useUserStore(pinia) // Pass explicitly
})
}
Load references/ssr-and-nuxt.md for complete SSR and Nuxt integration guide.
State Hydration:
devalue() (not JSON.stringify)useStore()useStore() BEFORE await in actionsbunx nuxi@latest module add pinia
Auto-imports: defineStore, storeToRefs, usePinia, acceptHMRUpdate, all stores
→ Load references/ssr-and-nuxt.md for: Complete SSR patterns, Nuxt configuration, server-side data fetching, SSR pitfalls, debugging
Load references/testing-guide.md for complete testing guide.
import { setActivePinia, createPinia } from 'pinia'
beforeEach(() => {
setActivePinia(createPinia()) // Fresh Pinia for each test
})
bun add -d @pinia/testing
import { createTestingPinia } from '@pinia/testing'
mount(Component, {
global: { plugins: [createTestingPinia()] }
})
→ Load references/testing-guide.md for: Complete test patterns, stubbing actions, mocking getters, async testing, SSR testing
// stores/counter.ts
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useCounterStore = defineStore('counter', {
// store definition
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
}
if (import.meta.webpackHot) {
import.meta.webpackHot.accept(acceptHMRUpdate(useCounterStore, import.meta.webpackHot))
}
Benefits:
For projects still using Options API, load complete mapper documentation.
→ Load references/state-getters-actions.md for: Complete Options API integration, all mappers (mapStores, mapState, mapWritableState, mapActions)
Load references/vuex-migration.md for complete migration guide.
Key Changes:
namespaced (automatic via store ID)mutations (direct state mutation)commit() with direct mutationsrootState/rootGetters with store importsstore.$reset() instead of custom clear mutationsDirectory: store/modules/ → stores/ (each module = separate store)
→ Load references/vuex-migration.md for: Complete conversion steps, component migration, checklist, gradual migration strategy
✅ Define all state properties in state() or return them from setup stores
✅ Use storeToRefs() when destructuring state/getters in components
✅ Call app.use(pinia) BEFORE mounting the app
✅ Return all state from setup stores (private state breaks SSR/DevTools)
✅ Call useStore() inside functions/callbacks when used outside components
✅ Use acceptHMRUpdate() for development HMR support
✅ Type return values when getters use this to access other getters
✅ Use devalue for SSR state serialization (prevents XSS)
✅ Hydrate state BEFORE calling any useStore() on the client (SSR)
✅ Call all useStore() BEFORE any await in async actions (SSR)
❌ Add state properties dynamically after store creation
❌ Destructure store directly without storeToRefs() (loses reactivity)
❌ Use arrow functions for actions (need this context)
❌ Return private state in setup stores (breaks SSR/DevTools/plugins)
❌ Call useStore() at module top-level (before Pinia installed)
❌ Create circular dependencies between stores (both reading each other's state)
❌ Use JSON.stringify() for SSR serialization (vulnerable to XSS)
❌ Call useStore() after await in actions (breaks SSR)
❌ Forget to type getter return values when using this
❌ Skip beforeEach(() => setActivePinia(createPinia())) in unit tests
This skill prevents 12 documented issues:
Error: State changes don't update in template after destructuring
Why It Happens: JavaScript destructuring breaks Vue reactivity
Prevention: Always use storeToRefs() for state/getters
Error: New properties added after store creation aren't reactive
Why It Happens: Pinia needs all properties defined upfront for reactivity
Prevention: Declare all properties in state(), even if initially undefined
Error: getActivePinia() returns undefined
Why It Happens: Calling useStore() before app.use(pinia)
Prevention: Call app.use(pinia) before mounting or accessing stores
Error: State not serialized/hydrated correctly in SSR Why It Happens: Properties not returned from setup aren't tracked Prevention: Return ALL state properties from setup stores
this Don't Infer TypesError: TypeScript can't infer return type when getter uses this
Source: Known TypeScript limitation with Pinia
Prevention: Explicitly type return value: getterName(): ReturnType { ... }
Error: Can't find this.counterStore in component
Why It Happens: mapStores() automatically adds 'Store' suffix
Prevention: Use store name + 'Store' or call setMapStoreSuffix()
await Break SSRError: Wrong Pinia instance used in SSR, causing state pollution
Why It Happens: await changes execution context in async functions
Prevention: Call all useStore() before any await statements
Error: Maximum call stack exceeded Why It Happens: Both stores read each other's state during initialization Prevention: Use getters/actions for cross-store access, not setup-time reads
Error: User input in state can execute malicious scripts
Why It Happens: JSON.stringify() doesn't escape executable code
Prevention: Use devalue library for safe serialization
Error: Changes to store require full page reload
Why It Happens: Vite/webpack HMR not configured for store
Prevention: Add acceptHMRUpdate() block to each store file
Error: Store state contains non-serializable functions
Why It Happens: Option stores state() can only return writable refs
Prevention: Use setup stores for complex composables, or extract only writable state
Error: Tests affect each other, sporadic failures
Why It Happens: Single Pinia instance shared across tests
Prevention: beforeEach(() => setActivePinia(createPinia())) in test suites
Core: pinia@^3.0.4, vue@^3.5.24
Nuxt: @pinia/nuxt@^0.11.2, nuxt@^3.13.0
Testing: @pinia/testing@^1.0.2, vitest@^1.0.0
SSR: devalue@^5.3.2 (for safe serialization)
See reference files for complete pattern examples:
references/state-getters-actions.mdreferences/plugins-composables.mdreferences/store-syntax-guide.md (setup store examples)references/state-getters-actions.md (accessing stores outside components)Solution:
app.use(pinia) is called before mountinguseStore() inside callback/functionuseStore(pinia)Solution: Use storeToRefs() instead of direct destructuring
this has TypeScript errorsSolution: Explicitly type the return value: myGetter(): ReturnType { return this.otherGetter }
Solution: Implement custom reset manually:
function $reset() {
count.value = 0
name.value = ''
}
return { count, name, $reset }
Solution: Add HMR acceptance block:
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useMyStore, import.meta.hot))
}
Solution: Create fresh Pinia in beforeEach():
beforeEach(() => {
setActivePinia(createPinia())
})
Load references/store-syntax-guide.md when:
Load references/state-getters-actions.md when:
$patch, $subscribe, or $onActionmapStores, mapState, mapActions)Load references/plugins-composables.md when:
Load references/ssr-and-nuxt.md when:
Load references/testing-guide.md when:
createTestingPiniaLoad references/vuex-migration.md when:
pinia packagecreatePinia()app.use(pinia) before mountingsrc/stores/)defineStore()storeToRefs() when destructuring in componentsthisacceptHMRUpdate() (development)@pinia/nuxt (if using Nuxt)createTestingPinia() (if testing)use[Name]StoreQuestions? Issues?