frontend / vuejs
Pinia — gestion d'état
Explication
Ce que vous allez apprendre
- Comprendre pourquoi un store dédié devient nécessaire quand
provide/injectne suffit plus - Créer un store Pinia en syntaxe "Options" (state/getters/actions) et en syntaxe "Setup"
- Déstructurer un store sans perdre sa réactivité grâce à
storeToRefs - Écrire des actions asynchrones directement dans un store, sans distinction artificielle avec les actions synchrones
- Composer plusieurs stores entre eux
Dans quel contexte ?
Un panier d'achat doit être accessible et modifiable depuis la barre de navigation, la page produit, et la page de paiement — trois composants qui n'ont pas de lien parent/enfant direct entre eux. provide/inject (vu en leçon 9) devient vite limité pour ce genre d'état partagé et modifié à de nombreux endroits ; Pinia, le store officiel de Vue 3, est pensé précisément pour ce cas.
D'abord, pourquoi un store dédié plutôt que provide/inject partout
Pinia centralise l'état partagé de l'application dans des stores nommés, accessibles depuis N'IMPORTE QUEL composant via une simple fonction (usePanierStore()), sans avoir à organiser une chaîne de provide au bon endroit de l'arbre de composants. C'est le successeur officiel de Vuex, avec une API bien plus simple et un typage TypeScript natif.
Prérequis
Il faut avoir compris ref/reactive/computed (leçons 3 et 6) : la syntaxe "Setup" des stores Pinia s'appuie directement sur ces mêmes primitives.
Ensuite, deux syntaxes équivalentes pour écrire un store
La syntaxe "Options" (state, getters, actions) rappelle l'Options API vue en leçon 4 : familière, structurée par type. La syntaxe "Setup" (une simple fonction qui retourne ref/computed/fonctions) rappelle la Composition API : plus flexible, notamment pour composer de la logique complexe.
| Syntaxe | Ressemble à | Avantage |
|---|---|---|
| "Options" | Options API | Structure claire, familière |
| "Setup" | Composition API | Plus flexible pour de la logique avancée |
Les deux syntaxes produisent un store fonctionnellement identique : le choix est une question de préférence et de cohérence avec le reste du projet.
Il reste un piège à connaître avant de déstructurer un store
Exactement comme pour reactive() (leçon 3), déstructurer directement un store Pinia (const { articles, total } = panier) casse la réactivité de ces propriétés. storeToRefs(panier) résout ce problème en convertissant chaque state/getter en une véritable ref réactive.
Piège fréquent
storeToRefs ne convertit QUE le state et les getters, jamais les actions : les actions restent de simples fonctions qui se déstructurent normalement (const { ajouter, vider } = panier). Appliquer storeToRefs aux actions n'aurait d'ailleurs aucun sens, puisqu'une fonction n'a pas de valeur réactive à suivre.
Maintenant, des actions qui n'ont pas besoin de distinction synchrone/asynchrone
Contrairement à Vuex qui distinguait strictement les "mutations" (synchrones) des "actions" (potentiellement asynchrones), Pinia unifie tout dans les actions : async appliquerCodePromo(code) { ... } fonctionne exactement comme une action synchrone, avec await directement à l'intérieur.
Astuce
Un store peut appeler useAutreStore() directement en son sein (comme useCommandesStore qui utilise useUtilisateurStore), permettant une composition naturelle entre stores sans configuration supplémentaire ni système de modules imbriqués comme c'était le cas avec Vuex.
Maintenant que l'état global de l'application est bien géré, la prochaine leçon aborde comment réutiliser de la logique réactive complète (état + comportement) entre plusieurs composants, au-delà du seul état partagé : les composables.
Commandes & code
Pinia — gestion d'état
Le store officiel de Vue 3 (successeur de Vuex), simple, typé, et intégré à la Composition API.
npm install pinia// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
createApp(App).use(createPinia()).mount('#app')// stores/panier.js — syntaxe "Options" du store (proche de Options API, familière)
import { defineStore } from 'pinia'
export const usePanierStore = defineStore('panier', {
state: () => ({
articles: [], // [{ id, nom, prix, quantite }]
codePromo: null,
}),
getters: {
// équivalent des computed : dérivés, mis en cache, accessibles via store.total
total: (state) => state.articles.reduce((s, a) => s + a.prix * a.quantite, 0),
nombreArticles: (state) => state.articles.reduce((s, a) => s + a.quantite, 0),
// un getter peut dépendre d'un autre getter via `this` (uniquement en syntaxe non-fléchée)
totalAvecRemise() {
const remise = this.codePromo ? 0.9 : 1
return this.total * remise
},
},
actions: {
// les actions peuvent être synchrones OU asynchrones, et muter directement le state via `this`
ajouter(produit, quantite = 1) {
const existant = this.articles.find((a) => a.id === produit.id)
if (existant) {
existant.quantite += quantite
} else {
this.articles.push({ ...produit, quantite })
}
},
retirer(id) {
this.articles = this.articles.filter((a) => a.id !== id)
},
async appliquerCodePromo(code) {
const reponse = await fetch(`/api/promos/${code}`)
if (reponse.ok) this.codePromo = code
},
vider() {
this.articles = []
this.codePromo = null
},
},
})// stores/utilisateur.js — syntaxe "Setup" du store (Composition API pure, plus flexible)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUtilisateurStore = defineStore('utilisateur', () => {
const utilisateur = ref(null)
const chargement = ref(false)
const estConnecte = computed(() => utilisateur.value !== null)
const estAdmin = computed(() => utilisateur.value?.role === 'admin')
async function connecter(identifiants) {
chargement.value = true
try {
const reponse = await fetch('/api/connexion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(identifiants),
})
if (!reponse.ok) throw new Error('Identifiants invalides')
utilisateur.value = await reponse.json()
} finally {
chargement.value = false
}
}
function deconnecter() {
utilisateur.value = null
}
// tout ce qui est retourné est PUBLIC (équivalent state+getters+actions combinés)
return { utilisateur, chargement, estConnecte, estAdmin, connecter, deconnecter }
})<script setup>
import { storeToRefs } from 'pinia'
import { usePanierStore } from '../stores/panier'
const panier = usePanierStore()
// PIÈGE : déstructurer directement `panier` casse la réactivité (comme reactive()).
// storeToRefs() convertit state+getters (PAS les actions) en refs réactives.
const { articles, total, nombreArticles } = storeToRefs(panier)
// les actions, elles, se déstructurent normalement (ce sont de simples fonctions)
const { ajouter, vider } = panier
</script>
<template>
<p>{{ nombreArticles }} article(s) — Total : {{ total.toFixed(2) }} €</p>
<button @click="vider">Vider le panier</button>
</template>// Store composé : un store peut utiliser un autre store
import { defineStore } from 'pinia'
import { useUtilisateurStore } from './utilisateur'
export const useCommandesStore = defineStore('commandes', {
state: () => ({ historique: [] }),
actions: {
async passerCommande(articles) {
const utilisateurStore = useUtilisateurStore()
if (!utilisateurStore.estConnecte) throw new Error('Connexion requise')
const reponse = await fetch('/api/commandes', {
method: 'POST',
body: JSON.stringify({ articles, utilisateurId: utilisateurStore.utilisateur.id }),
})
this.historique.push(await reponse.json())
},
},
})Résumé
- Deux syntaxes équivalentes : "Options" (state/getters/actions) et "Setup" (ref/computed/fonctions).
storeToRefs(store)est indispensable pour déstructurer state/getters sans perdre la réactivité.- Les actions peuvent être async nativement, contrairement à Vuex qui distinguait mutations et actions.
- Un store peut appeler
useAutreStore()en son sein : la composition entre stores est directe et simple.
Exercices pratiques
Mission : un compteur d'articles qui ne bouge plus
Objectif : Diagnostiquer une déstructuration de store cassant la réactivité, puis corriger avec storeToRefs et ajouter une action asynchrone sûre.
Contexte
Dans la barre de navigation, un badge affiche le nombre d'articles du panier via const { nombreArticles } = usePanierStore() directement dans <script setup>, sans storeToRefs. Le badge affiche bien "0" au chargement, mais reste bloqué à cette valeur même après avoir ajouté plusieurs articles au panier depuis la page produit, alors que le store lui-même contient bien les bons articles (vérifié via les devtools Pinia).