React
Shared state with Pinia: stores that stay singular
Pinia keeps state outside components: one store per domain, a setup store, storeToRefs for reactivity and one instance per request on the server.
Key points
- One store per domain: cart, session, filters. Never one per component.
- storeToRefs keeps reactivity when you destructure state and getters; actions are called on the store.
- On the server the Pinia instance must be created per request, never at module level.
Shared state with Pinia lives in an importable store, not in provide/inject: a component reads the store and does not know who else uses it. One store per domain, not one per component.
One store, one domain
The first argument of defineStore is the id: it is the key devtools and persistence plugins use to recognise the store. Two stores with the same id are the same store, even when you declare them in different files.
npm install pinia
// stores/cart.ts
import { computed, ref } from "vue";
import { defineStore } from "pinia";
export type CartItem = { id: string; qty: number; price: number };
export const useCart = defineStore("cart", () => {
const items = ref([]);
const count = computed(() => items.value.reduce((n, i) => n + i.qty, 0));
const total = computed(() => items.value.reduce((n, i) => n + i.qty * i.price, 0));
function add(item: CartItem) {
const found = items.value.find((i) => i.id === item.id);
if (found) found.qty += item.qty;
else items.value.push({ ...item });
}
function clear() {
items.value = [];
}
return { items, count, total, add, clear };
});
A setup store is a function: refs become state, computed values become getters, functions become actions. There is no mutation layer to write: the action changes the state, and devtools records the call.
// main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
createApp(App).use(createPinia()).mount("#app");
storeToRefs: the line that keeps reactivity
Destructuring the store copies values and stops updates. storeToRefs returns connected refs for state and getters; actions are called on the store and never destructured, because they lose their context.
import { storeToRefs } from "pinia";
import { useCart } from "@/stores/cart";
const cart = useCart();
const { count, total } = storeToRefs(cart);
Cart ({{ count }}) — {{ total.toFixed(2) }}
SSR: one instance per request
On the server an imported module is shared by every request. Call createPinia() at module level and one visitor's cart ends up in another visitor's page: create the instance inside the function that builds the app, once per request.
Where the store breaks
- Calling
useCart()outsidesetup()with no active Pinia instance: the error only shows at runtime. - Relying on
$resetin a setup store: it exists in options stores, everywhere else you write it yourself. - Persisting class instances with the persistence plugin: on reload they come back as plain objects, without methods.
- Keeping
windoworlocalStoragein the store body: during server rendering that code does not exist.
The limitation to plan for
Pinia is not a database: it does not deduplicate requests, does not sync two tabs and does not resolve conflicts. If two components write the same field, the last action wins. Persistence is a plugin you configure and test, not a promise of the framework.
The components that read the store are ready: Vuetify, shadcn-vue and the dashboard block bring markup and states, not the state. The base is in the Composition API; the case where reactivity is lost is in props that do not update, the comparison in Vue vs React.
Frequently asked questions
One store per component or one per domain?
One per domain. A store describes shared data such as a cart or a session; creating one per component pulls state back into the tree and loses the benefit.
Setup store or options store?
Setup store if you already use the Composition API and want to call composables inside the store. Options stores stay more readable for people coming from Vuex who want an automatic reset.
Why does the counter not update in the template?
Because you destructured the store without storeToRefs and copied the number. Keep the whole store or use storeToRefs for state and getters.
How do I persist a store?
With a plugin such as pinia-plugin-persistedstate, choosing which fields to save. Persistence is not in the core and must be treated as an integration to test.
Does the store work with SSR?
Yes, on one condition: one Pinia instance per request. Create the instance once at module level and one visitor's state reaches another.