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.

Create a free account to save this page, take notes and track your progress. Sign in

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/nuxt module does this).

The code

A setup store, storeToRefs, and per-request state in SSR
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 } = store and then wondering why the UI doesn't update; use storeToRefs.
  • Putting everything in global stores; keep local UI state in components and server data in data-fetching composables.
  • Module-level ref/reactive state 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.

Was this page helpful?