Retour au cours

frontend / vuejs

Pinia — gestion d'état

Leçon 141 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi un store dédié devient nécessaire quand provide/inject ne 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.

SyntaxeRessemble àAvantage
"Options"Options APIStructure claire, familière
"Setup"Composition APIPlus 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.

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

createApp(App).use(createPinia()).mount('#app')
js
// 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
    },
  },
})
js
// 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 }
})
vue
<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>
js
// 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

1 disponible
1

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).

Résoudre l’exercice →