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 da setup() senza istanza Pinia attiva: l’errore arriva solo a runtime.
  • Contare su $reset in 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 window o localStorage nel 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.