Vue Pinia
Manage shared Vue application state with focused, reactive stores.
Overview
Pinia is Vue's official state management library. It places shared state and related business logic in stores that components can access without passing props through many levels. Stores expose reactive state, derived getters, and actions and integrate with TypeScript and Vue Devtools.
Key concepts
- State contains shared reactive data
- Getters derive values from store state
- Actions perform updates and business workflows
- Separate stores by feature or domain
What is Pinia?
Pinia is a library for centralized application state. Instead of components keeping separate copies of the same cart, account, or preference data, they read and update one store.
Why do we need Pinia?
In an online store, adding a product must update the cart page, header badge, checkout total, and perhaps saved cart data. Passing the same state through several unrelated component levels creates prop drilling. A Pinia cart store gives every relevant component one consistent source.
When should you use Pinia?
- State is needed by distant components
- Several routes share the same data
- Business logic should remain outside presentation components
- State transitions need debugging and Devtools visibility
- A feature benefits from one explicit source of truth
Keep local UI state—such as whether one component's menu is open—inside that component unless other areas genuinely need it.
Installing Pinia
npm install piniaRegistering Pinia
Create one Pinia instance and install it before mounting the application.
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
app.use(createPinia());
app.mount("#app");Creating an Options Store
defineStore receives a unique store ID and an object containing state, getters, and actions.
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++;
}
}
});Using a Store in a Component
<script setup>
import { useCounterStore } from "@/stores/counter";
const counter = useCounterStore();
</script>
<template>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
</template>Every component using this store sees the same reactive count and updates when an action changes it.
State
State is the store's source data. It can be read and directly assigned, updated through actions, reset with $reset in an Options Store, or changed in groups with $patch.
counter.count++;
counter.$patch({
count: 10
});
counter.$reset();Getters
Getters are store-level computed values. They derive information without duplicating it in state.
getters: {
itemCount: (state) =>
state.items.reduce((total, item) => total + item.quantity, 0),
cartTotal: (state) =>
state.items.reduce(
(total, item) => total + item.price * item.quantity,
0
)
}Actions
Actions contain state changes and business workflows. They can be synchronous or asynchronous and can call other stores.
actions: {
addItem(product) {
const item = this.items.find((entry) => entry.id === product.id);
if (item) item.quantity++;
else this.items.push({ ...product, quantity: 1 });
},
async loadCart() {
const response = await fetch("/api/cart");
if (!response.ok) throw new Error("Unable to load cart");
this.items = await response.json();
}
}Setup Stores
A Setup Store uses Composition API primitives. Returned refs become state, computed values become getters, and functions become actions.
import { computed, ref } from "vue";
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});Destructuring Store State
Directly destructuring state from a store breaks its reactive connection. Use storeToRefs for state and getters; actions can be destructured normally.
import { storeToRefs } from "pinia";
const counter = useCounterStore();
const { count, doubleCount } = storeToRefs(counter);
const { increment } = counter;Cart Store example
import { defineStore } from "pinia";
export const useCartStore = defineStore("cart", {
state: () => ({ items: [] }),
getters: {
itemCount: (state) =>
state.items.reduce((sum, item) => sum + item.quantity, 0),
total: (state) =>
state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
},
actions: {
add(product) {
const existing = this.items.find((item) => item.id === product.id);
if (existing) existing.quantity++;
else this.items.push({ ...product, quantity: 1 });
}
}
});The header reads itemCount, product cards call add, the cart renders items, and checkout reads total. All use one reactive store.
State Persistence
Pinia state does not persist across a full page reload by default. Persistence requires deliberate storage code or a compatible plugin. Never store secrets or authoritative security data in browser storage.
Pinia and Server Data
A store can cache API results, but distinguish client-owned application state from server-owned data. Define refresh, error, loading, and invalidation behavior instead of assuming fetched data remains current forever.
Benefits of Pinia
- Centralized shared state
- Reactive updates across components
- Less prop drilling
- Clear state, getter, and action responsibilities
- Vue Devtools integration
- Strong TypeScript inference
- Modular stores
Common beginner mistakes
- Forgetting app.use(createPinia())
- Putting every local value in a store
- Creating one enormous unrelated store
- Duplicating the same source data across stores
- Directly destructuring state without storeToRefs
- Putting business logic in every component
- Assuming state persists automatically
- Storing sensitive information in browser persistence
Best practices
- Create stores around domains or features
- Keep state minimal and derive values with getters
- Place reusable business workflows in actions
- Keep temporary component UI state local
- Use storeToRefs when destructuring
- Represent loading and error states
- Avoid circular store dependencies
- Test important actions independently
- Persist only intentional non-sensitive state
Key takeaways
- Pinia is Vue's official state library
- Stores centralize shared reactive data
- State stores source values
- Getters derive cached values
- Actions perform updates and asynchronous workflows
- Options and Setup Store styles are supported
- storeToRefs preserves reactivity during destructuring
- Local state should remain local
- Well-scoped stores make large Vue applications easier to maintain