What is Pinia?
Pinia is the official state management library for Vue. It replaced Vuex as the recommended store in Vue 3 and is now the default in new Vue and Nuxt projects. It is small, type-safe and built directly on the Composition API.
The design is intentionally minimal. A store is a function that returns state, derived values and actions. There are no mutations, no module namespacing and no per-store provider. If you know ref, computed and functions, you already know most of Pinia.
Defining a store
A store is created with defineStore, which takes a unique id and a setup function.
// stores/cart.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCartStore = defineStore("cart", () => {
const items = ref([]);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0),
);
const count = computed(() => items.value.length);
function add(item) {
items.value.push(item);
}
function remove(id) {
items.value = items.value.filter((item) => item.id !== id);
}
return { items, total, count, add, remove };
});
This is a setup store: ref becomes state, computed becomes a getter, and functions become actions. The unique id names the store in devtools and is required.
Pinia also supports an options store if you prefer the Vuex-like shape:
// stores/counter.js
export const useCounter = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count += 1;
},
},
});
Both forms are fully supported. Setup stores tend to fit better with TypeScript and the Composition API, while options stores can feel familiar to Vuex users.
Using a store in components
Call the store function to get the store instance. There is no provider to wrap around your components.
<!-- Cart.vue -->
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";
const cart = useCartStore();
const { items, total } = storeToRefs(cart);
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} — {{ item.price }}
<button @click="cart.remove(item.id)">Remove</button>
</li>
</ul>
<p>Total: {{ total }}</p>
</template>
Reading cart.total directly in a template stays reactive. When you want to destructure state or getters while keeping reactivity, use storeToRefs, which converts them into refs. Actions can be destructured directly because they do not need reactivity.
Getters
Getters are computed values derived from state. They are cached and shared, so every component reading the same getter shares one calculation.
// stores/products.js
export const useProductsStore = defineStore("products", () => {
const products = ref([]);
const filter = ref("");
const visible = computed(() =>
products.value.filter((p) =>
p.name.toLowerCase().includes(filter.value.toLowerCase()),
),
);
const inStock = computed(() =>
visible.value.filter((p) => p.stock > 0),
);
return { products, filter, visible, inStock };
});
Reaching for watch to compute a value is a common mistake. If it can be derived from existing state, it should be a getter, exactly as you would use computed in a component.
Actions
Actions are functions that change state. They can be synchronous or async, and they can call other actions or even other stores.
// stores/users.js
export const useUsersStore = defineStore("users", () => {
const users = ref([]);
const loading = ref(false);
const error = ref(null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
users.value = await res.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
return { users, loading, error, fetchUsers };
});
For server data, though, consider a data-fetching layer such as TanStack Query, which handles caching and invalidation. Pinia is best for client state: carts, filters, UI preferences, auth state and drafts.
Plugins and persistence
Pinia plugins run for every store and can add behaviour such as persistence, logging or resetting.
// persist.js
export function persistPlugin({ store }) {
const saved = localStorage.getItem(store.$id);
if (saved) store.$patch(JSON.parse(saved));
store.$subscribe((_mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}
// main.js
const pinia = createPinia();
pinia.use(persistPlugin);
app.use(pinia);
The popular pinia-plugin-persistedstate does the same thing with options for which keys to store. Persistence is one of the main reasons to reach for a store instead of local component state.
Pinia with Nuxt
In Nuxt, install the official module and stores in stores/ are auto-imported.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
});
<!-- pages/cart.vue -->
<script setup>
const cart = useCartStore(); // auto-imported
</script>
Because Nuxt handles installation and server rendering, the same store works on the server and the client. See the Nuxt guide for how this fits with useFetch and server routes.
Best practices
- Prefer setup stores for new code, especially with TypeScript.
- Keep derived data in getters instead of watching state.
- Use
storeToRefswhen destructuring state or getters. - Keep server data in a data-fetching layer, not in Pinia.
- Use actions for anything that changes state, including async work.
- Persist only the state that should survive a reload.
- Name stores clearly by the domain they own.
Common mistakes
- Destructuring state without
storeToRefsand losing reactivity. - Using
watchto compute a value that a getter should derive. - Putting fetched server data in the store and hand-managing loading flags.
- Forgetting to install Pinia, so store calls fail at runtime.
- Creating one enormous store instead of focused domain stores.
- Mutating state from outside an action in a way that is hard to trace.
Where to go next
Pinia is the Vue equivalent of a small, modern store. Deepen your Vue knowledge, add Nuxt for server rendering and auto-imports, and compare the approach with React’s Context API, Zustand and Redux Toolkit. Then build a small store with a getter, an async action and persistence to see how little code it takes.