🤔 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
ref() et reactive() locaux — inutile de tout mettre dans un store.Pinia vs Vuex
| Pinia | Vuex |
|---|---|
| API simple et intuitive | Mutations séparées des actions |
| TypeScript natif | Typage laborieux |
| Devtools Vue intégrés | Devtools séparés |
| Stores modulaires par défaut | Modules complexes |
| Officiellement recommandé par Vue | Legacy (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 };
});
🔗 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éstructure | Comment |
|---|---|
| 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 };
});