Understanding Vue + Pinia stores
State, derived state, mutations — one defineStore call.
Pinia's setup-style stores, how they replaced Vuex, and the typed-by-construction property that makes them ergonomic.
Pinia is the official Vuex successor.
Pinia (2021) replaced Vuex as Vue's recommended state management as of Vue 3. Same conceptual idea (centralised stores for shared state), much smaller API. Vuex had mutations, actions, getters as three separate concepts; Pinia collapses them into one. Modern Vue projects use Pinia exclusively; Vuex remains for migration in older codebases.
Setup-style stores.
The recommended Pinia syntax mirrors Vue 3's Composition API. State is ref or reactive; computed values are computed; actions are plain functions. The store is a function that returns these. Typed by construction — TypeScript infers the store's shape from what the setup function returns, no boilerplate type declarations needed.
A worked store.
A counter store: import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() { count.value++ }
return { count, doubled, increment };
}); Three exports from the setup function: a writable ref, a computed derivation, an action. Components import useCounterStore(), get a typed object with all three. No reducers, no dispatch ceremony.
State + computed + action
ref + computed + function
Three pieces, one store.
defineStore('counter', () => ({ count, doubled, increment }))
= Typed, reactive store
Reading a store in a component.
const counter = useCounterStore() in a component's setup gives you a reactive proxy. Use counter.count in the template — Vue's reactivity tracks it. Outside the template (in setup logic), use counter.count directly; the proxy unwraps refs. Destructuring is the trap: const { count } = counter loses reactivity. Use storeToRefs(counter) to keep it.
Persistence and devtools.
Pinia integrates with Vue Devtools out of the box — every store change is visible, time-travel works. For localStorage / sessionStorage persistence, the pinia-plugin-persistedstate package adds one option per store. The plugin model is genuinely simple; writing custom plugins is a handful of lines.
When you don't need Pinia.
For component-local state, use ref directly — no store needed. For shared state across two or three sibling components, use Vue's provide/inject pattern. Pinia is the right answer when the state is truly application-wide (user session, app settings, cross- page caches). Reaching for a global store for one component's data is over- architecture; the rule is "lift state to the highest component that actually needs it, then Pinia only if that's effectively the root".