React
Stato condiviso con Pinia: store che non si duplicano
Pinia tiene lo stato fuori dai componenti: uno store per dominio, uno store setup, storeToRefs per la reattività e una istanza per richiesta in SSR.
In sintesi
- Uno store per dominio: carrello, sessione, filtri. Non uno per componente.
- storeToRefs tiene la reattività quando destrutturi stato e getter; le azioni si chiamano sullo store.
- In SSR l’istanza Pinia va creata per richiesta, mai a livello di modulo.
Lo stato condiviso con Pinia vive in uno store importabile, non in un provide/inject: il componente legge lo store e non sa chi altro lo sta usando. Uno store per dominio, non uno per componente.
Uno store, un dominio
Il primo argomento di defineStore è l’id: è la chiave con cui gli strumenti di sviluppo e i plugin di persistenza riconoscono lo store. Due store con lo stesso id sono lo stesso store, anche se li dichiari in file diversi.
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 };
});
Lo store setup è una funzione: i ref diventano stato, i computed diventano getter, le funzioni diventano azioni. Non c’è un livello di mutation da scrivere: l’azione modifica lo stato e basta, e lo strumento di sviluppo registra la chiamata.
// main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
createApp(App).use(createPinia()).mount("#app");
storeToRefs: la riga che tiene la reattività
Destrutturare lo store copia i valori e interrompe gli aggiornamenti. storeToRefs restituisce ref collegate per stato e getter; le azioni si chiamano sullo store, non si destrutturano, perché perdono il contesto.
import { storeToRefs } from "pinia";
import { useCart } from "@/stores/cart";
const cart = useCart();
const { count, total } = storeToRefs(cart);
Cart ({{ count }}) — {{ total.toFixed(2) }}
SSR: una istanza per richiesta
Sul server un modulo importato è condiviso da tutte le richieste. Se chiami createPinia() a livello di modulo, il carrello di un visitatore finisce nella pagina di un altro: l’istanza va creata dentro la funzione che costruisce l’app, una volta per richiesta.
Dove lo store si rompe
- Chiamare
useCart()fuori dasetup()senza istanza Pinia attiva: l’errore arriva solo a runtime. - Contare su
$resetin uno store setup: esiste negli store a opzioni, negli altri va scritto a mano. - Salvare istanze di classi con il plugin di persistenza: al ricaricamento tornano oggetti semplici, senza metodi.
- Tenere
windowolocalStoragenel corpo dello store: durante il rendering sul server quel codice non esiste.
Il limite da mettere in conto
Pinia non è un database: non deduplica le richieste, non sincronizza due schede e non risolve i conflitti. Se due componenti scrivono lo stesso campo, vince l’ultima azione. La persistenza è un plugin da configurare e testare, non una promessa del framework.
I componenti che leggono lo store sono già pronti: Vuetify, shadcn-vue e il blocco dashboard portano markup e stati, non lo stato. La base è in Composition API; il caso in cui la reattività si perde è in props che non si aggiornano, il confronto in Vue vs React.
Domande frequenti
Uno store per componente o uno per dominio?
Uno per dominio. Lo store descrive un dato condiviso, come carrello o sessione; crearlo per componente riporta lo stato dentro l’albero e perde il vantaggio.
Meglio store setup o store a opzioni?
Store setup se usi già la Composition API e vuoi chiamare composable dentro lo store. Lo store a opzioni resta più leggibile per chi arriva da Vuex e vuole reset automatico.
Perché il contatore non si aggiorna nel template?
Perché hai destrutturato lo store senza storeToRefs e hai copiato il numero. Tieni lo store intero oppure usa storeToRefs per stato e getter.
Come faccio la persistenza?
Con un plugin come pinia-plugin-persistedstate, scegliendo i campi da salvare. La persistenza non è nel core e va trattata come integrazione da testare.
Lo store funziona in SSR?
Sì, a una condizione: una istanza Pinia per richiesta. Se crei l’istanza una volta a livello di modulo, lo stato di un visitatore arriva a un altro.