🤔 Pourquoi un store ?

Dans une application Vue, les données peuvent être locales à un composant (ref()) ou partagées via props/emits. Mais quand plusieurs composants non liés doivent accéder aux mêmes données, le passage de props devient laborieux (prop drilling).

Le problème du prop drilling

<!-- ❌ Prop drilling sur 4 niveaux -->
<App user="..." />
  <Header user="..." />
    <Nav user="..." />
      <Avatar user="..." />  <!-- Seulement Avatar a besoin de user -->

Quand utiliser un store ?

  • Données partagées entre des composants non liés (panier, utilisateur connecté, thème)
  • État global persisté (localStorage)
  • Données chargées depuis une API et utilisées partout
  • Logique complexe avec plusieurs actions
💡 Pour les données locales à un composant (un compteur, un formulaire), continuez d'utiliser ref() et reactive() locaux — inutile de tout mettre dans un store.

Pinia vs Vuex

PiniaVuex
API simple et intuitiveMutations séparées des actions
TypeScript natifTypage laborieux
Devtools Vue intégrésDevtools séparés
Stores modulaires par défautModules complexes
Officiellement recommandé par VueLegacy (non recommandé pour nouveaux projets)

📦 Installation CDN & createPinia

En CDN, Pinia est disponible via unpkg. L'ordre de chargement est important : Vue d'abord, puis Pinia.

Chargement via CDN

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/pinia/dist/pinia.iife.js"></script>

<script>
// Destructurer depuis l'objet global Pinia
const { createPinia, defineStore, storeToRefs } = Pinia;
const { createApp, ref, computed } = Vue;
</script>

Brancher Pinia sur l'application

// ⚠️ Créer le pinia AVANT createApp (ou juste avant app.use)
const pinia = createPinia();

const app = createApp(App);
app.use(pinia);   // Enregistrer Pinia comme plugin
app.mount('#app');
⚠️ createPinia() doit être appelé avant app.mount(). Et defineStore() peut être défini avant la création de l'app — mais le store ne peut être utilisé (appelé) qu'après app.use(pinia).

🏗️ State, Getters, Actions

Un store Pinia est structuré autour de trois concepts fondamentaux.

Les 3 piliers de Pinia (Options Store)

const useCounterStore = defineStore('counter', {

  // STATE — données réactives du store (comme data() dans un composant)
  state: () => ({
    count: 0,
    nom: 'Compteur',
  }),

  // GETTERS — valeurs dérivées calculées (comme computed dans un composant)
  getters: {
    double: (state) => state.count * 2,
    isPositive: (state) => state.count > 0,
    // Getter qui utilise un autre getter (via this en Options Store)
    doublePositive() {
      return this.isPositive ? this.double : 0;
    },
  },

  // ACTIONS — fonctions qui modifient l'état (comme methods dans un composant)
  actions: {
    increment() {
      this.count++;    // 'this' = le store, accès direct au state
    },
    decrement() {
      this.count--;
    },
    reset() {
      this.count = 0;
    },
    // Les actions peuvent être asynchrones
    async fetchFromAPI() {
      const data = await fetch('/api/counter').then(r => r.json());
      this.count = data.value;
    },
  },
});

Utiliser le store dans un composant

const MonComposant = {
  setup() {
    const counter = useCounterStore();

    // Accès direct aux propriétés (réactif)
    // counter.count, counter.double, counter.isPositive

    // Appel des actions
    function augmenter() { counter.increment(); }
    function reinitialiser() { counter.reset(); }

    return { counter, augmenter, reinitialiser };
  },
  template: `
    <div>
      <p>Compteur : {{ counter.count }}</p>
      <p>Double : {{ counter.double }}</p>
      <button @click="augmenter">+</button>
      <button @click="reinitialiser">Reset</button>
    </div>
  `
};

🔄 Options Store vs Composition Store

Pinia supporte deux syntaxes pour définir un store — l'une ressemble à l'Options API Vue 2, l'autre à la Composition API Vue 3.

Options Store (style Vue 2)

const useUserStore = defineStore('user', {
  state: () => ({
    nom: '',
    email: '',
    role: 'user',
  }),
  getters: {
    nomComplet: (state) => state.nom || 'Anonyme',
    isAdmin: (state) => state.role === 'admin',
  },
  actions: {
    seConnecter(credentials) {
      this.nom = credentials.nom;
      this.email = credentials.email;
    },
    seDeconnecter() {
      this.$reset();  // Méthode spéciale qui remet le state à son état initial
    },
  },
});

Composition Store (style Vue 3 — recommandé)

const useUserStore = defineStore('user', () => {
  // STATE — refs
  const nom   = ref('');
  const email = ref('');
  const role  = ref('user');

  // GETTERS — computed
  const nomComplet = computed(() => nom.value || 'Anonyme');
  const isAdmin    = computed(() => role.value === 'admin');

  // ACTIONS — fonctions normales
  function seConnecter(credentials) {
    nom.value   = credentials.nom;
    email.value = credentials.email;
  }

  function seDeconnecter() {
    nom.value   = '';
    email.value = '';
    role.value  = 'user';
  }

  return { nom, email, role, nomComplet, isAdmin, seConnecter, seDeconnecter };
});
✅ La Composition Store est recommandée pour les nouveaux projets : meilleure inférence TypeScript, logique plus flexible, cohérence avec les composants Composition API.

🔗 storeToRefs() — déstructuration réactive

Si vous déstructurez directement un store Pinia, les propriétés perdent leur réactivité. storeToRefs() résout ce problème.

Le problème

const store = useCounterStore();

// ❌ Déstructuration classique — PERD la réactivité
const { count, double } = store;
// count et double sont des valeurs ordinaires, pas des refs
// Ils ne se mettent pas à jour quand store.count change !

La solution — storeToRefs()

const store = useCounterStore();

// ✅ storeToRefs — déstructuration RÉACTIVE
const { count, double } = storeToRefs(store);
// count et double sont des refs — ils restent synchronisés avec le store

// Les ACTIONS ne sont PAS des refs — les déstructurer directement
const { increment, reset } = store;  // ← directement depuis store, pas storeToRefs

return { count, double, increment, reset };

Récapitulatif

Ce qu'on déstructureComment
State et Getters (réactifs)storeToRefs(store)
Actions (fonctions)Directement depuis store

💾 Persistance avec localStorage

Pinia ne persiste pas le state automatiquement, mais c'est facile à implémenter avec des actions sauvegarder() / charger() ou avec un watch.

Approche 1 — actions save/load

const STORAGE_KEY = 'mon-store';

const useMonStore = defineStore('monStore', {
  state: () => ({
    items: [],
    preferences: { theme: 'dark' },
  }),
  actions: {
    sauvegarder() {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({
        items: this.items,
        preferences: this.preferences,
      }));
    },
    charger() {
      try {
        const data = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
        if (data.items)       this.items       = data.items;
        if (data.preferences) this.preferences = data.preferences;
      } catch (e) {
        console.error('Erreur de chargement:', e);
      }
    },
  },
});

Approche 2 — watch automatique (recommandée)

const useMonStore = defineStore('monStore', () => {
  const items = ref([]);

  // Charger depuis localStorage au démarrage
  function init() {
    try {
      const saved = localStorage.getItem('mon-store-items');
      if (saved) items.value = JSON.parse(saved);
    } catch {}
  }

  // Sauvegarder automatiquement à chaque changement
  watch(items, (newItems) => {
    localStorage.setItem('mon-store-items', JSON.stringify(newItems));
  }, { deep: true }); // deep: true car c'est un tableau/objet

  init(); // Charger au démarrage

  return { items };
});
✅ En production, utilisez le plugin officiel pinia-plugin-persistedstate qui gère automatiquement la persistance, la sérialisation, et supporte plusieurs stockages (sessionStorage, IndexedDB...).
✏️ Exercices V08 ▶ Mini-projet Panier Suivant : Composables →