Pinia
Pinia state management for Vue 3 including store design, actions, getters, plugins, and SSR hydration
You are an expert in Pinia, the official state management library for Vue 3, covering store architecture, TypeScript patterns, and integration with Nuxt. ## Key Points - **Store** — a reactive entity holding state, getters (computed), and actions (methods) - **Setup stores** — define stores using Composition API syntax (`ref`, `computed`, functions) - **Option stores** — define stores using an options object (`state`, `getters`, `actions`) - **Plugins** — extend every store with shared logic (persistence, logging, etc.) - **`storeToRefs`** — destructure a store while preserving reactivity for state and getters 1. **Prefer setup stores** — they offer better TypeScript inference and are closer to the Composition API you already use in components. 2. **One concern per store** — keep stores focused (auth, cart, ui). Compose stores when you need cross-cutting logic. 3. **Use `storeToRefs` for destructuring** — never destructure state or getters directly from the store object; reactivity will be lost. 4. **Actions for async and mutations** — keep all state mutations inside actions for traceability in devtools. 5. **Use `$reset()` for option stores** — option stores get a free `$reset()` method; for setup stores, implement your own reset function. 6. **Avoid storing derived data** — use getters (computed) instead of duplicating data in state. 7. **Use `$subscribe` sparingly** — it fires on every state change; debounce if you are persisting to storage.
skilldb get vue-skills/PiniaFull skill: 258 linesPinia State Management — Vue.js
You are an expert in Pinia, the official state management library for Vue 3, covering store architecture, TypeScript patterns, and integration with Nuxt.
Core Philosophy
Pinia is designed around the principle that state management should feel like a natural extension of the Composition API, not a separate paradigm with its own vocabulary. A Pinia store is just a function that returns reactive state, computed getters, and methods — the same primitives you already use inside components. This low conceptual overhead means developers spend time solving business problems rather than learning a state management framework.
Each store should own a single, well-defined domain: authentication, shopping cart, user preferences. This encourages thinking about state in terms of bounded contexts rather than a monolithic global store. When stores need to coordinate, they compose by calling each other directly within actions, keeping the dependency graph explicit and traceable. The absence of mutations (unlike Vuex) is a deliberate simplification — actions are the single entry point for all state changes, and devtools track them automatically.
Pinia's design also acknowledges that not all state belongs in a store. Component-local state should remain local. Stores are for state that genuinely needs to be shared across multiple components or that must persist across navigations. Reaching for a store by default when a simple ref inside a composable would suffice is a sign of over-engineering.
Anti-Patterns
-
Destructuring Stores Without
storeToRefs— writingconst { count } = useCounterStore()and expecting reactivity, when this actually gives you a plain non-reactive snapshot. Always usestoreToRefs()for state and getters. -
Circular Store Dependencies at Definition Time — store A calling
useStoreB()at the top level while store B callsuseStoreA()at the top level, creating an infinite loop. Call dependent stores inside actions, not during store initialization. -
Storing Derived Data as State — maintaining a
filteredListin state when it should be a getter derived fromlistandfilter. Duplicating derived data leads to stale values and synchronization bugs. -
Treating Every Piece of State as Global — creating stores for form state, UI toggles, or component-specific data that only one component uses. This adds indirection without benefit. Keep local state local.
-
Forgetting SSR Hydration Boundaries — modifying store state in a client-only plugin without accounting for server-rendered HTML, causing hydration mismatches that produce visual glitches and console warnings.
Overview
Pinia is Vue's recommended store library, replacing Vuex. It provides a simple API based on the Composition API, full TypeScript support, devtools integration, and SSR compatibility. Each store is an independent reactive module with state, getters, and actions.
Core Concepts
- Store — a reactive entity holding state, getters (computed), and actions (methods)
- Setup stores — define stores using Composition API syntax (
ref,computed, functions) - Option stores — define stores using an options object (
state,getters,actions) - Plugins — extend every store with shared logic (persistence, logging, etc.)
storeToRefs— destructure a store while preserving reactivity for state and getters
Implementation Patterns
Setup Store (Recommended)
// stores/useCartStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
function addItem(product: Omit<CartItem, 'quantity'>) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(id: string) {
items.value = items.value.filter(i => i.id !== id)
}
function clear() {
items.value = []
}
return { items, totalItems, totalPrice, addItem, removeItem, clear }
})
Option Store
// stores/useUserStore.ts
import { defineStore } from 'pinia'
interface User {
id: string
name: string
email: string
}
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
token: null as string | null,
}),
getters: {
isAuthenticated: (state) => !!state.token,
displayName: (state) => state.user?.name ?? 'Guest',
},
actions: {
async login(email: string, password: string) {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await res.json()
this.token = data.token
this.user = data.user
},
logout() {
this.token = null
this.user = null
},
},
})
Using a Store in Components
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/useCartStore'
const cart = useCartStore()
// Destructure state and getters as refs (reactive)
const { items, totalItems, totalPrice } = storeToRefs(cart)
// Actions can be destructured directly (they are plain functions)
const { addItem, removeItem, clear } = cart
</script>
<template>
<div>
<p>{{ totalItems }} items — ${{ totalPrice.toFixed(2) }}</p>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} x{{ item.quantity }}
<button @click="removeItem(item.id)">Remove</button>
</li>
</ul>
<button @click="clear">Clear cart</button>
</div>
</template>
Store Composition (Store Using Another Store)
// stores/useCheckoutStore.ts
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { useCartStore } from './useCartStore'
import { useUserStore } from './useUserStore'
export const useCheckoutStore = defineStore('checkout', () => {
const cart = useCartStore()
const user = useUserStore()
const canCheckout = computed(() =>
user.isAuthenticated && cart.totalItems > 0
)
async function placeOrder() {
if (!canCheckout.value) throw new Error('Cannot checkout')
await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${user.token}`,
},
body: JSON.stringify({ items: cart.items }),
})
cart.clear()
}
return { canCheckout, placeOrder }
})
Persistence Plugin
// plugins/piniaPersistedState.ts
import { type PiniaPluginContext } from 'pinia'
export function piniaPersistedState({ store }: PiniaPluginContext) {
const key = `pinia-${store.$id}`
// Hydrate from localStorage
const saved = localStorage.getItem(key)
if (saved) {
store.$patch(JSON.parse(saved))
}
// Persist on every mutation
store.$subscribe((_mutation, state) => {
localStorage.setItem(key, JSON.stringify(state))
})
}
// main.ts
import { createPinia } from 'pinia'
import { piniaPersistedState } from './plugins/piniaPersistedState'
const pinia = createPinia()
pinia.use(piniaPersistedState)
Pinia with Nuxt 3
// Nuxt auto-imports from stores/ directory when using @pinia/nuxt module
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
storesDirs: ['./stores/**'],
},
})
Best Practices
- Prefer setup stores — they offer better TypeScript inference and are closer to the Composition API you already use in components.
- One concern per store — keep stores focused (auth, cart, ui). Compose stores when you need cross-cutting logic.
- Use
storeToRefsfor destructuring — never destructure state or getters directly from the store object; reactivity will be lost. - Actions for async and mutations — keep all state mutations inside actions for traceability in devtools.
- Use
$reset()for option stores — option stores get a free$reset()method; for setup stores, implement your own reset function. - Avoid storing derived data — use getters (computed) instead of duplicating data in state.
- Use
$subscribesparingly — it fires on every state change; debounce if you are persisting to storage.
Common Pitfalls
- Destructuring without
storeToRefs—const { count } = useCounterStore()gives you a plain non-reactive value. UsestoreToRefs()for state and getters. - Using stores before Pinia is installed — calling
useXStore()beforeapp.use(pinia)throws. In Nuxt, the@pinia/nuxtmodule handles this. - Circular store dependencies — store A using store B which uses store A causes infinite loops. Call the dependent store inside an action, not at the store's top level.
- SSR hydration mismatch — if you modify store state in a plugin that only runs on the client, the server-rendered HTML will not match. Guard client-only logic with environment checks.
- Forgetting setup stores have no
$reset— unlike option stores, setup stores do not auto-generate$reset(). You must expose your own reset function.
Install this skill directly: skilldb add vue-skills
Related Skills
Composables
Building reusable custom composables in Vue 3 with proper typing, lifecycle management, and state sharing
Composition API
Patterns and best practices for Vue 3 Composition API including refs, reactive, computed, watchers, and lifecycle hooks
Nuxt Routing
Nuxt 3 file-based routing, dynamic routes, layouts, middleware, and navigation patterns
Nuxt Server
Nuxt 3 server routes, API endpoints, server middleware, and Nitro engine patterns for full-stack development
Provide Inject
Vue 3 provide/inject for dependency injection, deeply nested component communication, and plugin design
Vue Router
Vue Router 4 advanced patterns including navigation guards, lazy loading, scroll behavior, and typed routes