Vue 3 and Nuxt · Vue 3 Fundamentals
State Management with Pinia
Organize shared state with Pinia setup stores, use getters and actions, keep reactivity with storeToRefs, and avoid shared state across SSR requests.
What you'll build
A cart store written as a Pinia setup store (state, getters, actions), used the way components use it (with storeToRefs), observed with $subscribe, and then rendered for two server requests to show why server-side state must live in a per-request Pinia instance, not in module-level variables.
The idea
- Pinia stores hold state shared by many components. A setup store is a function: refs are state, computeds are getters, functions are actions.
- Destructuring a store loses reactivity for state and getters; use
storeToRefs(store)(actions can be destructured directly). - With SSR, the server handles many users in one process. Module-level state is shared across requests and leaks between users; create a fresh Pinia per request (Nuxt's
@pinia/nuxtmodule does this).
The code
import { createSSRApp, ref, computed } from "vue";
import { renderToString } from "vue/server-renderer";
import { createPinia, defineStore, setActivePinia, storeToRefs } from "pinia";
const useCartStore = defineStore("cart", () => {
const items = ref([]);
const count = computed(() => items.value.reduce((n, item) => n + item.qty, 0));
const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.qty, 0));
function add(name, price) {
const line = items.value.find((item) => item.name === name);
if (line) line.qty++;
else items.value.push({ name, price, qty: 1 });
}
return { items, count, total, add };
});
setActivePinia(createPinia());
const cart = useCartStore();
const { count, total } = storeToRefs(cart); // stay reactive
const { add } = cart; // actions can be destructured
cart.$subscribe((mutation) => console.log(` changed (${mutation.type})`), { flush: "sync" });
add("Mug", 8.5);
add("Mug", 8.5);
add("Teapot", 24);
console.log(`count ${count.value}, total $${total.value.toFixed(2)}`);
// SSR: a module-level "store" is shared by every request handled by this server process
const sharedCart = ref([]);
async function renderPage(user, item, usePinia) {
const app = createSSRApp({
setup() {
if (usePinia) {
const store = useCartStore();
store.add(item, 10);
return { names: computed(() => store.items.map((i) => i.name).join(", ")) };
}
sharedCart.value.push(item);
return { names: computed(() => sharedCart.value.join(", ")) };
},
template: `<p>cart: {{ names }}</p>`,
});
if (usePinia) app.use(createPinia()); // a fresh Pinia per request
console.log(`${user} sees ${await renderToString(app)}`);
}
console.log("module-level state:");
await renderPage("ana", "Mug", false);
await renderPage("ben", "Kettle", false);
console.log("per-request Pinia:");
await renderPage("ana", "Mug", true);
await renderPage("ben", "Kettle", true);
Output
changed (direct) changed (direct) changed (direct) count 3, total $41.00 module-level state: ana sees <p>cart: Mug</p> ben sees <p>cart: Mug, Kettle</p> per-request Pinia: ana sees <p>cart: Mug</p> ben sees <p>cart: Kettle</p>
Run this on your computer — see “Get Started” for setup.
Common mistakes
const { count } = storeand then wondering why the UI doesn't update; usestoreToRefs.- Putting everything in global stores; keep local UI state in components and server data in data-fetching composables.
- Module-level
ref/reactivestate in SSR apps, leaking one user's data to another. - Mutating store state from many places without actions, making changes hard to trace.
Key points
- Setup stores: refs are state, computeds are getters, functions are actions.
- storeToRefs keeps destructured state and getters reactive.
- In SSR, state must be per request: one Pinia instance per app instance.
Further reading: Pinia: defining a store · Pinia: SSR · Vue SSR: cross-request state pollution
Practice questions
Why use storeToRefs when destructuring a Pinia store?
Answer
The store is a reactive object; destructuring copies plain values. storeToRefs creates refs for state and getters so they stay reactive (actions can be destructured directly).
What is cross-request state pollution?
Answer
In SSR, module-level state lives in the server process and is shared by all requests, so one user's data can leak into another user's page. State must be created per request.
What makes a setup store's function become state, getters and actions?
Answer
Returned refs become state, computed values become getters, and functions become actions.
Open in the app to mark lessons complete, take notes and track your progress.