Pinia (state)
Pinia is the official Vue state library: typed stores, devtools, hot-reload, plugin system. Each store is a setup function (or options object) that exposes refs/computed/actions; components subscribe to only the slices they read. Replaces Vuex with a cleaner API.
Setup-style store with persistence, getters, actions
EXAMPLE
// stores/cart.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
// state
const items = ref<{ sku: string; qty: number; priceCents: number }[]>([]);
// getters (computed)
const count = computed(() => items.value.reduce((s, i) => s + i.qty, 0));
const totalCents = computed(() => items.value.reduce((s, i) => s + i.qty * i.priceCents, 0));
const isEmpty = computed(() => items.value.length === 0);
// actions
function add(sku: string, priceCents: number, qty = 1) {
const found = items.value.find((i) => i.sku === sku);
if (found) found.qty += qty;
else items.value.push({ sku, qty, priceCents });
}
function remove(sku: string) {
items.value = items.value.filter((i) => i.sku !== sku);
}
function clear() {
items.value = [];
}
async function checkout() {
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ items: items.value, totalCents: totalCents.value }),
});
if (!res.ok) throw new Error('checkout failed');
clear();
return res.json();
}
return { items, count, totalCents, isEmpty, add, remove, clear, checkout };
});
// 2) Use it in a component
<script setup lang='ts'>
import { useCartStore } from '@/stores/cart';
import { storeToRefs } from 'pinia';
const cart = useCartStore();
const { count, totalCents } = storeToRefs(cart); // keep reactivity when destructuring
</script>
<template>
<button @click='cart.add("sku-1", 4995)'>Add jacket</button>
<p>Cart: {{ count }} items — ${{ (totalCents / 100).toFixed(2) }}</p>
<button :disabled='cart.isEmpty' @click='cart.checkout'>Checkout</button>
</template>
// 3) Subscribe to changes (debug + persistence)
import { useCartStore } from '@/stores/cart';
const cart = useCartStore();
cart.$subscribe((mutation, state) => {
// mutation.type === 'direct' | 'patch object' | 'patch function'
localStorage.setItem('cart', JSON.stringify(state));
});
// 4) Hydrate from localStorage on app start
const saved = localStorage.getItem('cart');
if (saved) Object.assign(cart.$state, JSON.parse(saved));
// 5) Or use the official plugin
// npm i pinia-plugin-persistedstate
// import { createPinia } from 'pinia';
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
// const pinia = createPinia(); pinia.use(piniaPluginPersistedstate);
// 6) Options style (alternative to setup style)
// export const useCartStore = defineStore('cart', {
// state: () => ({ items: [] as Item[] }),
// getters: {
// count: (s) => s.items.reduce((a, i) => a + i.qty, 0),
// },
// actions: {
// add(item: Item) { this.items.push(item); },
// },
// });
// 7) Cross-store composition
// import { useUserStore } from './user';
// export const useOrdersStore = defineStore('orders', () => {
// const user = useUserStore();
// async function load() {
// if (!user.id) return;
// // fetch orders for user.id
// }
// return { load };
// });
// 8) SSR — call the store INSIDE the setup() / route guard, not at module level
// app.use(createPinia());
// const cart = useCartStore(); // OK inside setup()
// const cart = useCartStore(); // BAD at module top level (no active app)
// 9) Testing — pass setActivePinia(createPinia()) in beforeEach
// import { setActivePinia, createPinia } from 'pinia';
// beforeEach(() => setActivePinia(createPinia()));
// 10) Pitfalls
// - Forgetting storeToRefs() when destructuring -> reactivity lost
// - Heavy computed chains that recompute every keystroke
// - Storing API tokens in a Pinia store (use a secure storage layer)
// - Mutating state from outside an action (works, but bypasses devtools timeline)
Why it matters
`storeToRefs` is the helper everyone forgets, then debugs for an hour. Destructuring a Pinia store directly returns plain values; `storeToRefs(store)` returns refs so reactivity survives. Make it a reflex when pulling state into a component.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounter = defineStore('counter', {
state: () => ({ n: 0 }),
actions: { inc() { this.n++; } },
});
Try it Yourself »
Discussion
Loading…