AcademyVue
Pinia: State Management for Vue Apps
A Pinia store is a composable with a fixed shape — state, getters and actions — and understanding it as one demystifies most of what it does.
2 min readUpdated 3 exercises
A store is state, getters and actions
Pinia's defineStore composition-API form is a composable with three conventional parts, returned as one object:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// state
const items = ref([])
// getters — derived, cached like any computed
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0),
)
// actions — the only place state should change
function addItem(item) {
items.value.push(item)
}
return { items, total, addItem }
})Called from any component as const cart = useCartStore(), every caller gets the same store instance — unlike a plain composable, where each call creates independent state, defineStore registers the store once and hands back the same reactive object every time. That single shared instance, not anything about ref/computed themselves, is the entire mechanism behind "global state" in Pinia.
Why Pinia replaced Vuex
Vuex required a separate mutations layer purely so Vue DevTools could time-travel through state changes — every state update had to go through a mutation, and every async operation had to live in a separate action that committed a mutation when it finished. Pinia's reactivity is the same ref/reactive system every other part of Vue 3 uses, so DevTools can already observe changes directly; the mutations layer was scaffolding the new reactivity system no longer needs. An action in Pinia can be async and mutate state directly, in one function:
async function checkout() {
isLoading.value = true
try {
await api.post('/orders', { items: items.value })
items.value = []
} finally {
isLoading.value = false
}
}Destructuring a store loses reactivity the same way reactive() does
A store built with the composition-API syntax above is, under the hood, a reactive object — so pulling a property out with plain destructuring copies the current value and disconnects it from future updates, exactly the reactive trap from earlier in this path:
const cart = useCartStore()
const { total } = cart // frozen at whatever total was right now
import { storeToRefs } from 'pinia'
const { total } = storeToRefs(cart) // stays connected — Pinia's toRefsstoreToRefs exists for exactly this, and it only wraps state and getters — actions are plain functions and can be destructured directly (const { addItem } = cart is always safe, since a function has no reactive value to lose).
What interviewers check
Whether you know why Pinia dropped mutations rather than assuming it is "Vuex but newer" — and whether storeToRefs comes up unprompted the moment a store gets destructured, since forgetting it is the most common Pinia bug in production code.
Now practice it
3 exercisesReading this page is the cheap half. These are the exercises that make you use it.
- Pinia State · Plugins & SSRPlugins and SSR push past the basics into exactly the store-instance subtleties this page is building toward.QuizTake · HardQuizTake · Hard
- Performance · shallowRef & markRawshallowRef and markRaw matter most on large store state — the same reactivity trade-offs from a state angle.QuizTake · MediumQuizTake · Medium
- Router · Lazy Loading & TransitionsLazy-loaded routes often lazy-load the stores they depend on — the two systems interact more than they seem to.QuizTake · HardQuizTake · Hard
Read next
Composables: Reusable Stateful Logic
A composable is a plain function using ref, computed and watch — Vue's answer to a custom React hook, without the rules-of-hooks constraints.
3 min
Ref vs Reactive: Vue's State Primitives
ref wraps any value behind a .value getter/setter; reactive proxies an object directly. The trade-offs explain why most Vue code reaches for ref by default.
2 min