Suggest an editImprove this articleRefine the answer for “Pinia: state management in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Pinia** is Vue 3's official state management library. You define a store with `defineStore`, expose state as refs and getters as computeds, then call the same composable in any component to get the shared reactive instance. ```typescript const useCounterStore = defineStore('counter', () => { const count = ref(0) const double = computed(() => count.value * 2) function increment() { count.value++ } return { count, double, increment } }) ``` **Key rule:** use `storeToRefs()` when destructuring state or getters to keep them reactive.Shown above the full answer for quick recall.Answer (EN)Image**Pinia** is Vue 3's official state management library, a store system that lets any component read and update shared reactive state without passing props through the component tree. ## Theory ### TL;DR - Pinia stores work like a shared whiteboard in an office: any component reads or updates the data, changes show up everywhere immediately - No mutations, no modules - just reactive refs, computeds, and plain functions - Composition API style (setup function inside `defineStore`) is the recommended approach; stores feel like regular composables - Use Pinia when 2+ unrelated components need the same data; local `ref()` covers everything else - Vue 2 projects should stay on Vuex 4; Pinia targets Vue 3 only ### Quick example ```typescript // stores/counter.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCounterStore = defineStore('counter', () => { const count = ref(0) // state const double = computed(() => count.value * 2) // getter function increment() { count.value++ } // action return { count, double, increment } }) // In any component: const store = useCounterStore() store.increment() console.log(store.double) // 2 - reactive, updates everywhere ``` `defineStore` takes a unique ID (`'counter'`) and a setup function. Every component calling `useCounterStore()` gets the same reactive instance back. Define once, use anywhere. ### Key difference from Vuex Vuex splits state changes into mutations (synchronous, committed with `store.commit`) and actions (async, dispatched with `store.dispatch`). Pinia drops that split entirely. You write a function, it updates a ref, components re-render. That cuts boilerplate in half and gives TypeScript full inference without extra type declarations. Stores look like composables you already write every day. ### Two store styles Pinia supports two syntaxes. Most new code uses the Composition API style: ```typescript // Composition API style - recommended export const useUserStore = defineStore('user', () => { const profile = ref<User | null>(null) const isLoggedIn = computed(() => profile.value !== null) async function login(email: string, password: string) { profile.value = await api.login(email, password) } function logout() { profile.value = null } return { profile, isLoggedIn, login, logout } }) ``` The Options API style is also valid, closer to the Vuex mental model: ```typescript // Options API style - works, slightly weaker TypeScript inference export const useUserStore = defineStore('user', { state: () => ({ name: '', isLoggedIn: false }), getters: { initials: (state) => state.name.split(' ').map(n => n[0]).join('') }, actions: { async login(email: string, password: string) { const user = await api.login(email, password) this.name = user.name this.isLoggedIn = true }, logout() { this.$reset() } } }) ``` Both styles are supported long-term. The Composition API version is easier to test and gives better editor autocomplete. ### Using a store in components ```vue <script setup> import { storeToRefs } from 'pinia' import { useCounterStore } from '@/stores/counter' const store = useCounterStore() // storeToRefs keeps state and getters reactive when you destructure const { count, double } = storeToRefs(store) // Actions are plain functions, no wrapper needed const { increment } = store </script> <template> <p>Count: {{ count }} / Double: {{ double }}</p> <button @click="increment">+</button> </template> ``` `storeToRefs` is the one API you call every single time you use a store in a component. It wraps each state property and getter in a `ref` so destructuring does not break reactivity. Actions are plain functions, so they skip the wrapper. ### How Pinia handles reactivity When you call `defineStore`, Pinia creates a reactive proxy via Vue's `reactive()`. Your refs and computeds are wrapped in a singleton stored in a Map, keyed by the store ID. Any component that calls `useCounterStore()` gets that same proxy back. Vue's effect system tracks which properties each component reads, so writes automatically schedule re-renders. DevTools connect to this proxy to enable time-travel debugging and patch inspection. ### Pinia vs Vuex | Feature | Pinia | Vuex 4 | |---|---|---| | API style | Composition + Options | Options only | | Mutations | Not needed | Required | | TypeScript | Full inference | Manual typing | | Modules | Flat stores per ID | Nested module tree | | DevTools | Time-travel + patches | Basic logging | | Bundle size | ~1.5 KB | ~10 KB | | Vue 2 support | Via plugin | Native | | When to use | Vue 3 new projects | Vue 2/3 migration with existing modules | Many teams default to Pinia for any new Vue 3 project and only keep Vuex where a migration would touch too much working code. ### When to use Pinia - Auth state shared across routes: one `useUserStore` with an `isLoggedIn` getter - Shopping cart persisted across page loads: Pinia + `pinia-plugin-persistedstate` - App-wide theme or locale: small store, almost no setup - Vue 2 codebase already on Vuex: keep Vuex, migration is not urgent - Single isolated component with no shared data: plain `ref()` is enough - Nuxt 3 apps: Pinia is built in and the default recommendation ### Common mistakes 1. **Destructuring state without `storeToRefs`** ```typescript // Wrong - count becomes a plain number, no longer reactive const { count } = useCounterStore() // Right - count stays a Ref<number>, component updates on changes const { count } = storeToRefs(useCounterStore()) ``` The component renders once with the initial value and never updates again. This is the single most common Pinia mistake. 2. **Mutating state directly outside actions** ```typescript const store = useCounterStore() store.count++ // Technically works, but skips DevTools tracking and $subscribe callbacks ``` Pinia does not throw here, unlike Vuex in strict mode. But you lose change history in DevTools and any `$subscribe` listeners will not fire. Keep mutations inside store actions. 3. **Missing `await` in async actions** ```typescript // Wrong - state updates after the action has already returned async function fetchUser() { fetch('/api/user').then(r => { user.value = r.json() }) } // Right - state updates synchronously relative to the awaiting caller async function fetchUser() { const data = await fetch('/api/user').then(r => r.json()) user.value = data } ``` 4. **SSR hydration mismatch in Nuxt** In production Nuxt apps, this is the issue that catches teams most off guard. The store runs on the server without `localStorage`, then the client hydrates with different persisted data. ```typescript // Wrong - server and client may have different values const store = useUserStore() store.profile // null on server, populated on client from localStorage // Right - hydrate only on the client side if (process.client) store.$hydrate() ``` ### Real-world usage - Nuxt 3 e-commerce: cart state across pages, saved with `pinia-plugin-persistedstate` - Vuetify admin dashboards: theme and user preferences in a shared store - Multi-step forms with PrimeVue: validation state per step lives in a store, each step reads from the same source - Vitest unit tests: `setActivePinia(createPinia())` in `beforeEach`, then use stores normally with `vi.mock` for async calls ### Follow-up questions **Q:** How does Pinia keep state reactive across components? **A:** `useStore()` returns the same proxy instance every time. Vue's effect system tracks which properties each component reads and triggers re-renders on writes. **Q:** What is `storeToRefs` and when do you need it? **A:** It wraps each state property and getter in a `ref` so destructuring preserves reactivity. Actions are plain functions and do not need the wrapper. **Q:** How do two stores reference each other? **A:** Call the other store's composable inside an action, not at the top level of the setup function. `useCartStore` can call `useUserStore()` inside `checkout()` to check login status safely. **Q:** How do you test a Pinia store with Vitest? **A:** Call `setActivePinia(createPinia())` in `beforeEach`. Use the store composable directly and assert state changes. Use `vi.mock` for API dependencies. **Q:** What is the SSR gotcha with setup-style stores in Nuxt? (Senior level) **A:** The setup function runs on the server without DOM access. Any store that reads `localStorage` or `window` will throw. Guard reads with `if (process.client)` and use `$hydrate()` after mount to sync client state with persisted data. **Q:** How would you migrate a Vuex module to Pinia? **A:** Flatten the module into one `defineStore` call. Convert mutations to direct ref updates inside actions. Getters become computeds. The module namespace becomes the store ID. ## Examples ### Counter store ```typescript // stores/counter.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const double = computed(() => count.value * 2) function increment() { count.value++ } function reset() { count.value = 0 } return { count, double, increment, reset } }) // Usage const store = useCounterStore() store.increment() // count = 1 store.increment() // count = 2 console.log(store.double) // 4 store.reset() // count = 0 ``` Any component calling `useCounterStore()` sees the same `count`. No props, no events, no global event bus. ### E-commerce cart with async actions ```typescript // stores/cart.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCartStore = defineStore('cart', () => { const items = ref<{ id: number; name: string; price: number; qty: number }[]>([]) const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.qty, 0) ) async function addItem(productId: number) { const data = await fetch(`/api/products/${productId}`).then(r => r.json()) const existing = items.value.find(i => i.id === productId) if (existing) { existing.qty++ } else { items.value.push({ ...data, qty: 1 }) } } function removeItem(id: number) { items.value = items.value.filter(i => i.id !== id) } return { items, total, addItem, removeItem } }) // CartView.vue const cart = useCartStore() await cart.addItem(42) console.log(cart.total) // price of item 42, recomputed automatically ``` `total` recomputes automatically after `addItem` resolves. No manual notification needed anywhere in the tree. ### Store-to-store communication ```typescript // stores/cart.ts - calls useUserStore inside an action import { defineStore } from 'pinia' import { ref } from 'vue' import { useUserStore } from './user' export const useCartStore = defineStore('cart', () => { const items = ref([]) function checkout() { const userStore = useUserStore() // safe to call inside an action if (!userStore.isLoggedIn) { throw new Error('Login required') } // process payment... } return { items, checkout } }) // plugins/pinia-hydrate.ts (Nuxt 3) export default defineNuxtPlugin((nuxtApp) => { nuxtApp.hooks.hook('app:mounted', () => { const userStore = useUserStore() if (process.client) { userStore.$hydrate() // sync client state with persisted localStorage data } }) }) ``` Never call `useUserStore()` at the top level of another store's setup function. Only call it inside actions, where a Vue context is guaranteed to exist.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.