Back to Skills

Pinia V3

Pinia v3 Vue state management with defineStore, getters, actions. Use for Vue 3 stores, Nuxt SSR, Vuex migration, or encountering store composition, hydration, testing errors.

vuetesting

Skill Content

# Pinia v3 - Vue State Management

**Status**: 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

---

## Quick Start (5 Minutes)

### 1. Install Pinia

```bash
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:**
- Pinia is the official Vue state management library
- Provides better TypeScript support than Vuex
- Eliminates mutations and namespacing complexity
- Full DevTools support with time-travel debugging

### 2. Create and Register Pinia Instance

```typescript
// 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:**
- Install Pinia BEFORE using any store
- Call `app.use(pinia)` before mounting the app
- Only one Pinia instance per application (unless SSR)

### 3. Define Your First Store

```typescript
// 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++
    }
  }
})
```

### 4. Use Store in Components

```vue
<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>
```

---

## The Two Store Syntaxes

**Load `references/store-syntax-guide.md` for complete comparison of Option vs Setup stores.**

### Quick Overview

Pinia supports two store definition syntaxes:

**Option Stores:**
- Similar to Vue Options API
- Built-in `$reset()` method
- Best for: Simpler use cases, teams familiar with Vuex

**Setup Stores:**
- Uses Composition API pattern
- Full composables integration
- Best for: Advanced patterns, need watchers/VueUse integration

**→ Load `references/store-syntax-guide.md` for:** Complete syntax comparison, examples, choosing criteria

---

## State, Getters, and Actions

**Load `references/state-getters-actions.md` for complete API reference.**

### Quick Reference

**State:**
- Define in `state: () => ({...})` (option) or `ref()` (setup)
- Access directly: `store.count`
- Mutate directly: `store.count++` or `store.$patch({...})`
- Reset: `store.$reset()` (option stores only)

**Getters:**
- Computed properties: `getters: { double: (state) => state.count * 2 }`
- Access other getters with `this` (must type return value)

**Actions:**
- Business logic: `actions: { increment() { this.count++ } }`
- Can be async
- Access other stores directly

**Store Destructuring:**
```typescript
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

---

## Plugins and Composables

**Load `references/plugins-composables.md` for complete plugin and composables guide.**

### Plugin Basics

```typescript
pinia.use(({ store, options }) => {
  // Add properties to every store
  return { customProperty: 'value' }
})
```

### Composables Integration

**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)

---

## Using Stores Outside Components

### The Problem

Stores need the Pinia instance, which is auto-injected in components but not available in module scope.

### ❌ Wrong: Accessing Store at Module Level

```typescript
// router.ts
import { useUserStore } from '@/stores/user'

// ❌ Fails: Pinia not installed yet
const userStore = useUserStore()

router.beforeEach((to) => {
  if (userStore.isLoggedIn) { /* ... */ }
})
```

### ✅ Right: Accessing Store Inside Callbacks

```typescript
// 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.

### SSR: Explicit Pinia Instance

```typescript
// server-side
export function setupRouter(pinia) {
  router.beforeEach((to) => {
    const userStore = useUserStore(pinia) // Pass explicitly
  })
}
```

---

## Server-Side Rendering & Nuxt

**Load `references/ssr-and-nuxt.md` for complete SSR and Nuxt integration guide.**

### SSR Quick Reference

**State Hydration:**
- Server: Serialize with `devalue()` (not `JSON.stringify`)
- Client: Hydrate BEFORE calling `useStore()`
- Critical: Call all `useStore()` BEFORE `await` in actions

### Nuxt 3/4 Integration

```bash
bunx 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

---

## Testing

**Load `references/testing-guide.md` for complete testing guide.**

### Testing Quick Start

```typescript
import { setActivePinia, createPinia } from 'pinia'

beforeEach(() => {
  setActivePinia(createPinia()) // Fresh Pinia for each test
})
```

### Component Testing

```bash
bun add -d @pinia/testing
```

```typescript
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

---

## Hot Module Replacement (HMR)

### Vite Setup

```typescript
// 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))
}
```

### Webpack Setup

```typescript
if (import.meta.webpackHot) {
  import.meta.webpackHot.accept(acceptHMRUpdate(useCounterStore, import.meta.webpackHot))
}
```

**Benefits:**
- Edit stores without full page reload
- Preserve application state during development
- Faster development iteration

---

## Options API Usage

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`)

---

## Migrating from Vuex

**Load `references/vuex-migration.md` for complete migration guide.**

### Quick Conversion Overview

**Key Changes:**
1. Remove `namespaced` (automatic via store ID)
2. Eliminate `mutations` (direct state mutation)
3. Replace `commit()` with direct mutations
4. Replace `rootState`/`rootGetters` with store imports
5. Use `store.$reset()` instead of custom clear mutations

**Directory:** `store/modules/` → `stores/` (each module = separate store)

**→ Load `references/vuex-migration.md` for:** Complete conversion steps, component migration, checklist, gradual migration strategy

---

## Critical Rules

### Always Do

✅ 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)

### Never Do

❌ 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

---

## Known Issues Prevention

This skill prevents **12** documented issues:

### Issue #1: Lost Reactivity from Direct Destructuring
**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

### Issue #2: Cannot Add State Properties Dynamically
**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`

### Issue #3: Store Not Found Before Pinia Install
**Error**: `getActivePinia()` returns undefined
**Why It Happens**: Calling `useStore()` before `app.use(pinia)`
**Prevention**: Call `app.use(pinia)` before mounting or accessing stores

### Issue #4: Setup Store Private State Breaks SSR
**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

### Issue #5: Getters with `this` Don't Infer Types
**Error**: TypeScript can't infer return type when getter uses `this`
**Source**: Known TypeScript limitation with Pinia
**Prevention**: Explicitly type return value: `getterName(): ReturnType { ... }`

### Issue #6: Options API Store Suffix Confusion
**Error**: Can't find `this.counterStore` in component
**Why It Happens**: `mapStores()` automatically adds 'Store' suffix
**Prevention**: Use store name + 'Store' or call `setMapStoreSuffix()`

### Issue #7: Actions Called After `await` Break SSR
**Error**: 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

### Issue #8: Circular Store Dependencies Crash App
**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

### Issue #9: XSS Vulnerability in SSR State Serialization
**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

### Issue #10: HMR Doesn't Work in Development
**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

### Issue #11: Composables Return Functions Break Option Stores
**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

### Issue #12: State Not Reset Between Unit Tests
**Error**: Tests affect each other, sporadic failures
**Why It Happens**: Single Pinia instance shared across tests
**Prevention**: `beforeEach(() => setActivePinia(createPinia()))` in test suites

---

## Package Versions (Verified 2025-11-21)

**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)

---

## Common Patterns

See reference files for complete pattern examples:
- **Authentication stores** → `references/state-getters-actions.md`
- **Persistence plugins** → `references/plugins-composables.md`
- **Form stores** → `references/store-syntax-guide.md` (setup store examples)
- **Router integration** → `references/state-getters-actions.md` (accessing stores outside components)

---

## Official Documentation

- **Pinia**: https://pinia.vuejs.org/
- **Getting Started**: https://pinia.vuejs.org/getting-started.html
- **Core Concepts**: https://pinia.vuejs.org/core-concepts/
- **SSR Guide**: https://pinia.vuejs.org/ssr/
- **Nuxt Integration**: https://pinia.vuejs.org/ssr/nuxt.html
- **Testing**: https://pinia.vuejs.org/cookbook/testing.html
- **Vuex Migration**: https://pinia.vuejs.org/cookbook/migration-vuex.html
- **GitHub**: https://github.com/vuejs/pinia

---

## Troubleshooting

### Problem: "getActivePinia() was called with no active Pinia"
**Solution**:
1. Ensure `app.use(pinia)` is called before mounting
2. If outside component, call `useStore()` inside callback/function
3. For SSR, pass pinia instance explicitly: `useStore(pinia)`

### Problem: State changes don't update in template
**Solution**: Use `storeToRefs()` instead of direct destructuring

### Problem: Getter using `this` has TypeScript errors
**Solution**: Explicitly type the return value: `myGetter(): ReturnType { return this.otherGetter }`

### Problem: $reset() not available in setup store
**Solution**: Implement custom reset manually:
```typescript
function $reset() {
  count.value = 0
  name.value = ''
}
return { count, name, $reset }
```

### Problem: HMR not working for stores
**Solution**: Add HMR acceptance block:
```typescript
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useMyStore, import.meta.hot))
}
```

### Problem: Tests fail intermittently
**Solution**: Create fresh Pinia in `beforeEach()`:
```typescript
beforeEach(() => {
  setActivePinia(createPinia())
})
```

---

## When to Load References

**Load `references/store-syntax-guide.md` when:**
- Need detailed comparison between Option and Setup store syntaxes
- Deciding which syntax to use for a new store
- Questions about Option vs Setup stores trade-offs
- Need complete examples of both syntaxes

**Load `references/state-getters-actions.md` when:**
- Need complete API reference for state, getters, or actions
- Questions about `$patch`, `$subscribe`, or `$onAction`
- Implementing store composition patterns
- Using Options API mappers (`mapStores`, `mapState`, `mapActions`)
- Accessing stores outside components (router, plugins)

**Load `references/plugins-composables.md` when:**
- Creating custom Pinia plugins
- Integrating VueUse or other composables into stores
- Need persistence, routing, or logging plugin patterns
- Questions about TypeScript typing for plugins
- Advanced composables integration

**Load `references/ssr-and-nuxt.md` when:**
- Setting up server-side rendering
- Integrating with Nuxt 3/4
- Questions about state hydration or serialization
- SSR-related errors (wrong Pinia instance, hydration mismatch)
- Nuxt auto-imports or configuration
- Server-side data fetching patterns

**Load `references/testing-guide.md` when:**
- Setting up unit tests for stores
- Testing components that use Pinia stores
- Need to stub actions or mock getters
- Questions about `createTestingPinia`
- Testing SSR stores
- Vitest or testing framework integration

**Load `references/vuex-migration.md` when:**
- Migrating existing Vuex codebase to Pinia
- Questions about Vuex→Pinia conversion
- Need migration checklist or examples
- Gradual migration strategy needed

---

## Complete Setup Checklist

- [ ] Installed `pinia` package
- [ ] Created Pinia instance with `createPinia()`
- [ ] Registered with `app.use(pinia)` before mounting
- [ ] Created stores directory (e.g., `src/stores/`)
- [ ] Defined at least one store with `defineStore()`
- [ ] Used `storeToRefs()` when destructuring in components
- [ ] Typed getter return values when using `this`
- [ ] Added HMR support with `acceptHMRUpdate()` (development)
- [ ] Configured SSR hydration (if using SSR)
- [ ] Configured `@pinia/nuxt` (if using Nuxt)
- [ ] Set up testing with `createTestingPinia()` (if testing)
- [ ] All stores follow consistent naming: `use[Name]Store`
- [ ] Verified DevTools integration works

---

**Questions? Issues?**

1. Check official docs: https://pinia.vuejs.org/
2. Review "Known Issues Prevention" section above
3. Verify setup checklist is complete
4. Check for TypeScript configuration issues
5. Ensure Pinia is installed before using stores

How to use

  1. Copy the skill content above
  2. Create a .claude/skills directory in your project
  3. Save as .claude/skills/claude-skills-pinia-v3.md
  4. Use /claude-skills-pinia-v3 in Claude Code to invoke this skill
View source on GitHub