Pinia Tutorial: Gestione dello Stato Avanzata e Semplificata per Vue.js 3

Intermedio
JavaScript Vue.js

Scopri Pinia, la soluzione di gestione dello stato leggera e intuitiva per le applicazioni Vue.js 3. Questo tutorial approfondito ti guiderà dalla configurazione di base ai concetti avanzati, con esempi pratici e best practice.

Pubblicato
Tag
javascript TypeScript Frontend Development Vue.js Web Programming state-management Pinia Vue 3

Introduzione a Pinia: La Gestione dello Stato Semplificata per Vue.js

Nel mondo dello sviluppo front-end, la gestione dello stato è una delle sfide più significative, specialmente in applicazioni complesse e di grandi dimensioni. In Vue.js, per molti anni, Vuex è stata la soluzione de facto per affrontare questa complessità. Tuttavia, con l'avvento di Vue 3 e l'introduzione della Composition API, è emersa la necessità di una soluzione più leggera, intuitiva e che sfruttasse appieno le nuove capacità reattive del framework.

È qui che entra in gioco Pinia. Pinia è il gestore dello stato consigliato dal team di Vue.js per Vue 3 e ha rapidamente guadagnato popolarità grazie alla sua API minimalista, al supporto TypeScript di prima classe e alla sua architettura modulare. Non è un semplice rimpiazzo di Vuex, ma piuttosto una sua evoluzione, progettata per essere più facile da imparare e da usare, riducendo al minimo il boilerplate e migliorando l'esperienza di sviluppo.

Questo tutorial è pensato per sviluppatori Vue.js di livello intermedio che desiderano padroneggiare Pinia. Esploreremo i suoi concetti fondamentali, impareremo a creare e integrare gli store nelle nostre applicazioni, e approfondiremo le best practice e le funzionalità avanzate. Alla fine di questo articolo, avrai una comprensione solida di Pinia e sarai in grado di applicarlo con fiducia nei tuoi progetti.

Perché Scegliere Pinia?

La scelta di un gestore dello stato non è mai banale. Ecco i principali motivi per cui Pinia si distingue:

  • Semplificazione: La sua API è incredibilmente snella e ispirata alla Composition API di Vue.js, rendendo la scrittura e la lettura del codice molto più intuitive.
  • Supporto TypeScript Eccezionale: Pinia è stato progettato con TypeScript in mente fin dall'inizio, offrendo inferenza di tipo completa e autocompletamento per uno sviluppo più robusto e privo di errori.
  • Modularità per Natura: Ogni store Pinia è un modulo a sé stante, il che favorisce la divisione del codice e la manutenibilità, senza la necessità di registrare moduli dinamici come in Vuex.
  • Nessuna Mutazione Esplicita: A differenza di Vuex, Pinia non richiede mutazioni esplicite. Le azioni possono modificare direttamente lo stato, semplificando il flusso di dati e riducendo il boilerplate.
  • Leggero e Performante: Pinia è estremamente leggero, con un bundle size ridotto. Inoltre, offre funzionalità come il hot module replacement e la possibilità di gestire più istanze dello stesso store, utili in ambienti di test o in applicazioni complesse.
  • Integrazione con Vue Devtools: Offre un'integrazione completa con Vue Devtools per un'esperienza di debugging senza pari, permettendo di tracciare le modifiche dello stato e le chiamate alle azioni.

Comprendere le Basi di Pinia

Prima di immergerci nel codice, è fondamentale comprendere i blocchi costitutivi di Pinia. Sebbene Pinia sia molto più semplice di Vuex, condivide alcuni concetti chiave ma li implementa in modo più elegante.

Il Concetto di "Store"

In Pinia, l'unità fondamentale per la gestione dello stato è lo "Store". Uno Store è essenzialmente un contenitore che detiene lo stato della tua applicazione. Ogni store è unico e può essere pensato come un pezzo specifico del tuo stato globale. Ad esempio, potresti avere uno store per l'autenticazione (authStore), uno per gli elementi di un carrello (cartStore), o uno per i dati degli utenti (userStore).

Uno store Pinia è composto da tre parti principali:

  1. State: Lo stato è il cuore dello store. Contiene i dati reattivi della tua applicazione. Pensalo come la proprietà data di un componente Vue, ma condivisa tra più componenti.
  2. Getters: I getters sono funzioni che derivano lo stato. Sono l'equivalente delle proprietà computate per lo stato dello store. Permettono di filtrare, calcolare o trasformare i dati dello stato prima di esporli ai componenti, e sono reattivi e memorizzati nella cache (cached).
  3. Actions: Le azioni sono funzioni che contengono la logica di business per modificare lo stato. Possono essere asincrone, effettuare chiamate API e coordinare più modifiche allo stato. A differenza di Vuex, le azioni in Pinia possono modificare direttamente lo stato.

Installazione e Setup Iniziale

L'installazione di Pinia è un processo semplice. Assicurati di avere un progetto Vue 3 funzionante.

Per prima cosa, installa Pinia tramite npm o yarn:

npm install pinia
# oppure
yarn add pinia

Una volta installato, devi collegare Pinia alla tua istanza dell'applicazione Vue. Questo viene fatto tipicamente nel file main.js (o main.ts se usi TypeScript):

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

In questo frammento di codice, creiamo un'istanza di Pinia utilizzando createPinia() e la passiamo all'applicazione Vue con app.use(pinia). Questo rende Pinia disponibile per tutti i componenti all'interno dell'applicazione.

Creare il Tuo Primo Store Pinia

Ora che abbiamo compreso i concetti di base e configurato Pinia, è il momento di creare il nostro primo store. Inizieremo con un semplice store per un contatore, un classico esempio per illustrare la gestione dello stato.

Per convenzione, gli store Pinia vengono spesso definiti in file separati all'interno di una directory stores (o store) nella radice del progetto, ad esempio src/stores/counter.js.

Definire un Store con defineStore

Pinia utilizza la funzione defineStore per definire gli store. Questa funzione accetta due argomenti principali:

  1. Un ID univoco per lo store (una stringa).
  2. Un oggetto di opzioni che definisce lo state, i getters e le actions dello store.

Vediamo come definire il nostro counterStore:

// src/stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  // Lo 'state' è una funzione che restituisce lo stato iniziale
  state: () => ({
    count: 0,
    name: 'Pinia User'
  }),

  // I 'getters' sono come le proprietà computate per lo stato dello store
  getters: {
    doubleCount: (state) => state.count * 2,
    greeting: (state) => `Hello, ${state.name}! Your count is ${state.count}`,
    // I getters possono accedere ad altri getters tramite 'this'
    doubleCountPlusOne() {
      return this.doubleCount + 1;
    }
  },

  // Le 'actions' sono funzioni che possono modificare lo stato e contenere logica di business
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
    incrementBy(amount) {
      this.count += amount;
    },
    async fetchRandomNumber() {
      // Simulazione di una chiamata API asincrona
      const response = await new Promise(resolve => setTimeout(() => resolve(Math.floor(Math.random() * 100)), 1000));
      this.count = response;
    }
  }
});

Analizziamo ogni parte di questo store:

  • export const useCounterStore = defineStore('counter', { ... }): Definiamo e esportiamo una funzione chiamata useCounterStore. Il primo argomento di defineStore è l'ID univoco dello store, 'counter'. Questo ID è cruciale e viene utilizzato da Pinia internamente e dai Vue Devtools per identificare lo store. È buona norma che l'ID corrisponda al nome del file (senza .js o .ts).
  • state: () => ({ count: 0, name: 'Pinia User' }): La proprietà state è una funzione che deve restituire l'oggetto dello stato iniziale. Questo garantisce che ogni store istanziato sia indipendente. Qui, abbiamo un semplice contatore count e una stringa name.
  • getters: { ... }: La proprietà getters contiene funzioni che derivano lo stato. Ogni getter riceve lo state come primo argomento e può accedere ad altri getters tramite this. doubleCount raddoppia il valore di count, mentre greeting crea un messaggio personalizzato. doubleCountPlusOne mostra come un getter possa usare un altro getter.
  • actions: { ... }: La proprietà actions contiene le funzioni che modificano lo stato e incapsulano la logica di business. Le azioni possono essere sincrone (increment, decrement, incrementBy) o asincrone (fetchRandomNumber). Notare come le azioni possono accedere e modificare direttamente lo stato tramite this.

Questo è il modello standard per definire uno store Pinia. È pulito, conciso e sfrutta la potenza della Composition API di Vue 3.

Integrare Pinia nei Componenti Vue

Dopo aver definito uno store, il passo successivo è utilizzarlo all'interno dei componenti Vue. Pinia rende questo processo incredibilmente semplice e intuitivo, specialmente con la Composition API.

Utilizzare useStore() nella Composition API

Per accedere a uno store all'interno di un componente Vue, si utilizza la funzione che abbiamo esportato (ad esempio, useCounterStore). Questa funzione è un hook composable che ci permette di ottenere un'istanza dello store.

Vediamo come utilizzare il nostro counterStore in un componente Vue:

<!-- src/components/CounterComponent.vue -->
<template>
  <div>
    <h2>Contatore Pinia</h2>
    <p>Conteggio: {{ counter.count }}</p>
    <p>Conteggio Raddoppiato: {{ counter.doubleCount }}</p>
    <p>Saluto: {{ counter.greeting }}</p>
    <button @click="counter.increment()">Incrementa</button>
    <button @click="counter.decrement()">Decrementa</button>
    <button @click="counter.incrementBy(5)">Incrementa di 5</button>
    <button @click="counter.fetchRandomNumber()">Imposta Numero Casuale (Async)</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '../stores/counter';

// Ottieni un'istanza dello store
const counter = useCounterStore();

// Puoi anche destrutturare lo stato direttamente, ma attenzione alla reattività
// const { count, name } = storeToRefs(counter); // Per mantenere la reattività dello stato
// const { increment, decrement } = counter; // Le azioni non richiedono storeToRefs

// Oppure, se vuoi destrutturare e mantenere la reattività per lo stato
// import { storeToRefs } from 'pinia';
// const { count, name, doubleCount, greeting } = storeToRefs(counter);
// const { increment, decrement, incrementBy, fetchRandomNumber } = counter;
</script>

<style scoped>
/* Stili del componente */
div {
  border: 1px solid #ccc;
  padding: 20px;
  margin: 20px;
  border-radius: 8px;
}
button {
  margin: 5px;
  padding: 8px 15px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
button:hover {
  background-color: #368a68;
}
</style>

Spiegazione:

  1. import { useCounterStore } from '../stores/counter';: Importiamo la funzione del nostro store.
  2. const counter = useCounterStore();: All'interno del blocco <script setup>, chiamiamo la funzione useCounterStore() per ottenere un'istanza dello store. L'istanza counter è un oggetto reattivo che contiene lo stato, i getters e le azioni del nostro store.
  3. {{ counter.count }} e {{ counter.doubleCount }}: Accediamo direttamente alle proprietà dello stato e ai getters tramite l'istanza counter. Grazie alla reattività di Vue, qualsiasi modifica allo stato nel Pinia store si rifletterà automaticamente nel componente.
  4. @click="counter.increment()": Chiamiamo le azioni dello store direttamente come metodi sull'istanza counter.

storeToRefs() per la Destrutturazione dello Stato

Un aspetto importante da considerare quando si destrutturano le proprietà dello stato da uno store Pinia è la perdita di reattività. Se si destruttura semplicemente const { count } = counter;, count non sarà più reattivo. Per mantenere la reattività, Pinia fornisce l'utility storeToRefs().

<script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '../stores/counter';

const counterStore = useCounterStore();

// Destruttura le proprietà dello stato e i getters, mantenendo la reattività
const { count, doubleCount, greeting } = storeToRefs(counterStore);

// Le azioni possono essere destrutturate direttamente perché sono solo funzioni
const { increment, decrement, incrementBy, fetchRandomNumber } = counterStore;
</script>

<template>
  <div>
    <h2>Contatore Pinia (con destrutturazione)</h2>
    <p>Conteggio: {{ count }}</p>
    <p>Conteggio Raddoppiato: {{ doubleCount }}</p>
    <p>Saluto: {{ greeting }}</p>
    <button @click="increment()">Incrementa</button>
    <button @click="decrement()">Decrementa</button>
    <button @click="incrementBy(5)">Incrementa di 5</button>
    <button @click="fetchRandomNumber()">Imposta Numero Casuale (Async)</button>
  </div>
</template>

L'uso di storeToRefs è una best practice quando si desidera destrutturare lo stato e i getters per una maggiore concisione nel template, senza compromettere la reattività.

Concetti Avanzati e Best Practice

Pinia offre molte più funzionalità oltre alle basi. Esploriamo alcuni concetti avanzati che ti aiuteranno a costruire applicazioni più robuste e manutenibili.

Modularità e Organizzazione degli Store

Uno dei maggiori vantaggi di Pinia è la sua modularità intrinseca. Ogni defineStore crea un modulo indipendente. Per progetti più grandi, è consigliabile organizzare gli store in file e cartelle separate, raggruppandoli per funzionalità.

Ad esempio:

src/
├── components/
├── stores/
│   ├── auth.js
│   ├── cart.js
│   ├── user.js
│   └── index.js  // Per esportare tutti gli store da un unico punto
└── main.js

Nel file src/stores/index.js, potresti esportare tutti i tuoi store per facilitare l'importazione in altri moduli:

// src/stores/index.js
export * from './auth';
export * from './cart';
export * from './user';
// ...e così via per tutti i tuoi store

Questo approccio mantiene il tuo codice pulito e ben organizzato, rendendo facile trovare e gestire lo stato correlato.

Integrazione con TypeScript

Pinia è stato progettato pensando a TypeScript, e la sua integrazione è uno dei suoi punti di forza. L'inferenza di tipo funziona out-of-the-box per la maggior parte dei casi, ma puoi rendere i tuoi store ancora più robusti tipizzando esplicitamente lo stato, i getters e le azioni.

Ecco come potresti tipizzare il tuo counterStore:

// src/stores/counter.ts
import { defineStore } from 'pinia';

// 1. Definisci l'interfaccia per lo stato
interface CounterState {
  count: number;
  name: string;
}

export const useCounterStore = defineStore('counter', {
  state: (): CounterState => ({
    count: 0,
    name: 'Pinia User'
  }),

  getters: {
    doubleCount(state): number {
      return state.count * 2;
    },
    greeting(state): string {
      return `Hello, ${state.name}! Your count is ${state.count}`;
    }
  },

  actions: {
    increment() {
      this.count++;
    },
    incrementBy(amount: number) {
      this.count += amount;
    },
    async fetchRandomNumber(): Promise<void> {
      const response: number = await new Promise(resolve => setTimeout(() => resolve(Math.floor(Math.random() * 100)), 1000));
      this.count = response;
    }
  }
});

Con TypeScript, ottieni:

  • Autocompletamento: Intellisense completo per lo stato, i getters e le azioni.
  • Controllo degli errori in fase di compilazione: TypeScript ti avviserà se tenti di accedere a una proprietà inesistente o di passare un tipo di dato errato a un'azione.
  • Codice più leggibile e manutenibile: Le interfacce definiscono chiaramente la struttura dei dati, migliorando la comprensione del codice.

Plugin Pinia

Pinia supporta un sistema di plugin che ti permette di estendere le funzionalità degli store. I plugin possono intercettare le chiamate alle azioni, modificare lo stato, aggiungere nuove proprietà o metodi agli store, o persino implementare la persistenza dello stato.

Un esempio comune è un plugin per persistere lo stato nel localStorage o sessionStorage.

// src/plugins/pinia-persist.js
export function piniaPersistPlugin({
  store
}) {
  const data = localStorage.getItem(store.$id);
  if (data) {
    store.$patch(JSON.parse(data));
  }

  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state));
  });
}

Per usarlo, devi registrarlo con l'istanza di Pinia in main.js:

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { piniaPersistPlugin } from './plugins/pinia-persist';

const app = createApp(App)
const pinia = createPinia()

pinia.use(piniaPersistPlugin);

app.use(pinia)
app.mount('#app')

Questo semplice plugin salverebbe lo stato di ogni store nel localStorage ogni volta che cambia e lo caricherebbe all'avvio dell'applicazione. Esistono librerie di terze parti come pinia-plugin-persistedstate che offrono soluzioni più robuste e configurabili per la persistenza.

Testabilità

Uno dei vantaggi di Pinia è la sua facilità di testing. Poiché ogni store è una funzione JavaScript pura, puoi testarli isolatamente senza montare componenti Vue.

// tests/unit/counter.spec.js (esempio con Vitest)
import { describe, it, expect, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { useCounterStore } from '../../src/stores/counter';

describe('Counter Store', () => {
  beforeEach(() => {
    // Crea una nuova istanza Pinia prima di ogni test
    // Questo è cruciale per evitare che i test si influenzino a vicenda
    setActivePinia(createPinia());
  });

  it('should increment the count', () => {
    const counter = useCounterStore();
    expect(counter.count).toBe(0);
    counter.increment();
    expect(counter.count).toBe(1);
  });

  it('should increment the count by a specific amount', () => {
    const counter = useCounterStore();
    counter.incrementBy(5);
    expect(counter.count).toBe(5);
  });

  it('should return the double count', () => {
    const counter = useCounterStore();
    counter.count = 2;
    expect(counter.doubleCount).toBe(4);
  });

  it('should fetch a random number asynchronously', async () => {
    const counter = useCounterStore();
    await counter.fetchRandomNumber();
    expect(counter.count).toBeGreaterThanOrEqual(0);
    expect(counter.count).toBeLessThanOrEqual(99);
  });
});

Con setActivePinia(createPinia()), ci assicuriamo che ogni test abbia la sua istanza isolata di Pinia, prevenendo effetti collaterali tra i test.

Esempi Pratici: Un Todo List Manager con Pinia

Per consolidare la nostra comprensione, creeremo un esempio più complesso: un semplice gestore di liste di cose da fare (Todo List) utilizzando Pinia.

Questo esempio ci permetterà di esplorare come gestire un array di oggetti nello stato, utilizzare getters per filtrare i dati e azioni per manipolare l'array.

Definizione dello Store todosStore

Creiamo un nuovo store in src/stores/todos.js:

// src/stores/todos.js
import { defineStore } from 'pinia';

export const useTodosStore = defineStore('todos', {
  state: () => ({
    todos: [
      { id: 1, text: 'Imparare Pinia', completed: true },
      { id: 2, text: 'Costruire un'app Vue con Pinia', completed: false },
      { id: 3, text: 'Scrivere un articolo su Pinia', completed: false }
    ],
    nextId: 4
  }),

  getters: {
    // Restituisce solo i todo non completati
    uncompletedTodos: (state) => state.todos.filter(todo => !todo.completed),
    // Restituisce solo i todo completati
    completedTodos: (state) => state.todos.filter(todo => todo.completed),
    // Restituisce il numero totale di todo
    totalTodos: (state) => state.todos.length,
    // Restituisce un todo per ID
    getTodoById: (state) => (id) => state.todos.find(todo => todo.id === id)
  },

  actions: {
    addTodo(text) {
      if (text.length === 0) return;
      this.todos.push({
        id: this.nextId++,
        text,
        completed: false
      });
    },
    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    removeTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
    },
    clearCompleted() {
      this.todos = this.todos.filter(todo => !todo.completed);
    }
  }
});

In questo store abbiamo:

  • State: Un array todos contenente oggetti todo e un nextId per generare ID univoci.
  • Getters: uncompletedTodos, completedTodos per filtrare i todo, totalTodos per contare e getTodoById come getter che accetta un parametro.
  • Actions: addTodo, toggleTodo, removeTodo e clearCompleted per manipolare l'array todos.

Componente Vue per la Todo List

Ora creiamo un componente Vue (src/components/TodoList.vue) che utilizzerà questo store:

<!-- src/components/TodoList.vue -->
<template>
  <div class="todo-list-container">
    <h2>La Mia Lista di Cose da Fare</h2>

    <div class="input-section">
      <input
        v-model="newTodoText"
        @keyup.enter="addTodoItem"
        placeholder="Aggiungi un nuovo todo..."
      />
      <button @click="addTodoItem">Aggiungi</button>
    </div>

    <h3>Todo Non Completati ({{ uncompletedTodos.length }})</h3>
    <ul class="todo-list">
      <li v-for="todo in uncompletedTodos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="toggleTodo(todo.id)"
        />
        <span>{{ todo.text }}</span>
        <button class="remove-btn" @click="removeTodo(todo.id)">X</button>
      </li>
    </ul>

    <h3>Todo Completati ({{ completedTodos.length }})</h3>
    <ul class="todo-list completed">
      <li v-for="todo in completedTodos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="toggleTodo(todo.id)"
        />
        <span>{{ todo.text }}</span>
        <button class="remove-btn" @click="removeTodo(todo.id)">X</button>
      </li>
    </ul>

    <div class="actions-section">
      <button @click="clearCompleted" :disabled="completedTodos.length === 0">Pulisci Completati</button>
      <p>Totale Todo: {{ totalTodos }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useTodosStore } from '../stores/todos';

const todosStore = useTodosStore();

// Destruttura lo stato e i getters reattivamente
const { todos, uncompletedTodos, completedTodos, totalTodos } = storeToRefs(todosStore);

// Destruttura le azioni
const { addTodo, toggleTodo, removeTodo, clearCompleted } = todosStore;

const newTodoText = ref('');

const addTodoItem = () => {
  addTodo(newTodoText.value);
  newTodoText.value = '';
};
</script>

<style scoped>
.todo-list-container {
  font-family: 'Arial', sans-serif;
  max-width: 600px;
  margin: 40px auto;
  padding: 25px;
  border: 1px solid #e0e0e0;
  border-radius: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  background-color: #fff;
}
h2 {
  text-align: center;
  color: #333;
  margin-bottom: 25px;
}
h3 {
  color: #555;
  border-bottom: 1px solid #eee;
  padding-bottom: 10px;
  margin-top: 25px;
  margin-bottom: 15px;
}
.input-section {
  display: flex;
  margin-bottom: 25px;
}
.input-section input {
  flex-grow: 1;
  padding: 12px 15px;
  border: 1px solid #ddd;
  border-radius: 5px;
  font-size: 1rem;
  margin-right: 10px;
}
.input-section button {
  padding: 12px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.2s ease;
}
.input-section button:hover {
  background-color: #0056b3;
}
.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.todo-list li {
  display: flex;
  align-items: center;
  padding: 12px 0;
  border-bottom: 1px solid #eee;
}
.todo-list li:last-child {
  border-bottom: none;
}
.todo-list li input[type="checkbox"] {
  margin-right: 15px;
  width: 18px;
  height: 18px;
  cursor: pointer;
}
.todo-list li span {
  flex-grow: 1;
  font-size: 1.05rem;
  color: #444;
}
.todo-list.completed li span {
  text-decoration: line-through;
  color: #888;
}
.remove-btn {
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 6px 10px;
  font-size: 0.85rem;
  cursor: pointer;
  transition: background-color 0.2s ease;
}
.remove-btn:hover {
  background-color: #c82333;
}
.actions-section {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 30px;
  padding-top: 15px;
  border-top: 1px solid #eee;
}
.actions-section button {
  padding: 10px 18px;
  background-color: #6c757d;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 0.95rem;
  transition: background-color 0.2s ease;
}
.actions-section button:hover:not(:disabled) {
  background-color: #5a6268;
}
.actions-section button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}
.actions-section p {
  font-size: 1rem;
  color: #666;
  margin: 0;
}
</style>

Questo componente dimostra pienamente come Pinia faciliti la gestione dello stato in un'applicazione reale. Lo stato dei todo è centralizzato e accessibile da qualsiasi parte dell'applicazione, mentre i componenti rimangono focalizzati sulla presentazione dei dati e sull'interazione utente.

Errori Comuni e Suggerimenti per la Risoluzione

Anche se Pinia è progettato per essere semplice, ci sono alcuni errori comuni in cui gli sviluppatori possono incorrere. Conoscerli può aiutarti a debugar più velocemente.

  1. Dimenticare app.use(pinia) in main.js: Se i tuoi store non sembrano funzionare o ottieni errori relativi a Pinia non inizializzato, controlla sempre il tuo file main.js (o main.ts). Senza app.use(pinia), Pinia non sarà disponibile per la tua applicazione Vue.
  2. ID dello Store non Univoco: Ogni store definito con defineStore() deve avere un ID univoco. Se definisci due store con lo stesso ID, Pinia genererà un errore. Assicurati che ogni store abbia un ID stringa distinto.
  3. Perdita di Reattività con la Destrutturazione: Come discusso, se destrutturi lo stato direttamente (es. const { count } = useCounterStore();), perderai la reattività. Ricorda di usare storeToRefs() per destrutturare le proprietà dello stato e i getters se vuoi che rimangano reattivi: const { count } = storeToRefs(useCounterStore());.
  4. Mutare lo Stato Direttamente Fuori dalle Azioni (in Options API): Sebbene Pinia permetta alle azioni di mutare direttamente lo stato (a differenza di Vuex), è una buona pratica incapsulare tutte le modifiche allo stato all'interno delle azioni. Questo mantiene la logica di business centralizzata e facilita il debugging (specialmente con i Vue Devtools).
  5. Accesso allo Store prima dell'inizializzazione: Se tenti di accedere a uno store in un contesto che non è ancora parte dell'applicazione Vue (ad esempio, in un modulo JavaScript standalone che non è ancora stato importato da un componente), potresti ottenere un errore. Assicurati che l'istanza di Pinia sia stata creata e utilizzata con app.use() prima di tentare di accedere agli store in contesti reattivi.
  6. Problemi con SSR (Server-Side Rendering): Se stai utilizzando Pinia con SSR (es. Nuxt.js), potresti dover prestare attenzione all'idratazione dello stato. Pinia ha un supporto eccellente per SSR, ma è importante seguire la documentazione specifica del tuo framework (es. Nuxt 3) per assicurarti che lo stato venga correttamente serializzato e deserializzato tra server e client.

Pinia vs. Vuex: Quando Scegliere Cosa

È naturale chiedersi quando scegliere Pinia rispetto a Vuex, soprattutto se si ha già familiarità con quest'ultimo. Ecco una breve guida:

  • Pinia: È la scelta consigliata per tutti i nuovi progetti Vue 3. I suoi vantaggi (semplicità, TypeScript, modularità, leggerezza) lo rendono superiore a Vuex nella maggior parte degli scenari moderni. Se stai iniziando un nuovo progetto Vue 3 o stai migrando un'applicazione Vue 2/Vuex a Vue 3, Pinia è la via da seguire.
  • Vuex: È ancora rilevante per progetti legacy Vue 2 che non prevedono una migrazione a breve termine. Se hai un'applicazione Vue 2 esistente con Vuex già implementato e funzionante, non c'è una necessità immediata di migrare a Pinia, a meno che tu non stia già pianificando un aggiornamento completo a Vue 3. Vuex 4 è la versione per Vue 3, ma Pinia è considerata la sua evoluzione e il successore preferito.

In sintesi, per il futuro dello sviluppo Vue, Pinia è la direzione chiara e la scelta preferita dal team di Vue.js.

Prossimi Passi e Risorse

Congratulazioni! Hai completato una panoramica approfondita di Pinia, dalla configurazione di base ai concetti avanzati e agli esempi pratici. Ora sei pronto per integrare Pinia nei tuoi progetti Vue.js 3.

Per approfondire ulteriormente, ti consiglio i seguenti passi:

  1. Consulta la Documentazione Ufficiale di Pinia: La documentazione di Pinia è eccellente, chiara e ricca di esempi. È la risorsa definitiva per qualsiasi dubbio o per esplorare funzionalità che non abbiamo coperto in dettaglio qui (es. composable actions, gestione delle sottoscrizioni, etc.). Puoi trovarla su https://pinia.vuejs.org/.
  2. Esplora i Plugin Ufficiali e della Community: Cerca plugin Pinia per funzionalità specifiche come la persistenza dello stato, il logging o l'integrazione con altre librerie. La community è molto attiva.
  3. Costruisci un Progetto Reale: Il modo migliore per imparare è fare. Prova a convertire un'applicazione Vuex esistente a Pinia o a costruire un nuovo progetto da zero utilizzando Pinia per la gestione dello stato. Scegli un'applicazione di dimensioni medio-grandi per sperimentare la modularità e la gestione di store interconnessi.
  4. Approfondisci TypeScript: Se non l'hai già fatto, investi tempo nell'apprendimento di TypeScript. La sua combinazione con Pinia e Vue 3 offre un'esperienza di sviluppo notevolmente migliorata in termini di robustezza e manutenibilità del codice.

Pinia è uno strumento potente che semplifica notevolmente la gestione dello stato in Vue.js, permettendoti di concentrarti sulla logica della tua applicazione piuttosto che sulla complessità del framework di gestione dello stato. Buon divertimento con Pinia!