iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…