Adloun

Pinia : un état partagé par toute l'application

Cours · Vue : construire une application web (école d'ingénieur), chapitre 10 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

Depuis le chapitre 7, le panier de la Librairie est créé dans le composant racine et fourni à ses descendants par provide. Le procédé a fait ses preuves, mais l'application à plusieurs écrans du chapitre précédent en montre les limites. Chaque composant qui touche au panier doit connaître la clé d'injection ; les valeurs fournies sont des ref qu'il faut déballer avec .value, jusque dans les gabarits. Surtout, ce qui n'est pas fourni par la racine vit dans une page : le catalogue est rechargé depuis le serveur chaque fois qu'on y revient, parce que la page qui le détenait a été détruite entre-temps.

Ce chapitre présente Pinia, la bibliothèque officielle de Vue pour l'état partagé : des stores, objets réactifs uniques dans l'application, qu'un composant obtient par un simple appel de fonction. On y range le panier et le catalogue de la Librairie. En chemin, on verra ce qu'il ne faut pas faire avec un store (le déstructurer sans précaution), et pourquoi un composable écrit pour les composants doit être adapté pour servir dans un store.

Objectifs du chapitre

  • Distinguer l'état local d'un composant de l'état partagé par l'application, et savoir où ranger chacun.
  • Écrire un store en style setup : état, accesseurs et actions.
  • Utiliser un store dans un composant sans perdre la réactivité (storeToRefs).
  • Observer un store, le modifier en bloc, reconnaître un store écrit avec l'API d'options.
  • Utiliser un composable dans un store, un store dans un autre, un store dans une garde du routeur.
  • Remplacer le panier injecté de la Librairie par un store persistant, et garder le catalogue en mémoire d'une page à l'autre.

Le chapitre utilise Pinia 4.0.3. Comme au chapitre précédent, les rendus ont été produits dans Chromium sur les projets construits, et les sorties marquées « test » par Vitest (chapitre 11).

10.1 Où ranger l'état ?

Une donnée qui change, dans une application Vue, peut vivre à quatre endroits.

Définition 10.1Store

Un store est un objet réactif, unique dans l'application, qui regroupe un état partagé, des accesseurs (valeurs calculées à partir de l'état) et des actions (fonctions qui le modifient). Tout composant qui l'utilise obtient le même objet ; ce que l'un modifie, tous les autres le voient.

Un module JavaScript qui exporte un objet reactive fait déjà office de store rudimentaire. Pinia y ajoute ce qui manque à une vraie application : l'inspection de chaque store et de chaque action dans Vue DevTools ; un état propre à chaque application, que les tests recréent à neuf ; des extensions (persistance, journal) ; une structure commune à tous les stores, que tout développeur Vue reconnaît.

Repère historique : De Vuex à Pinia

Vue 2 avait son gestionnaire d'état officiel, Vuex, inspiré de l'architecture Flux de Facebook : un état unique, modifié seulement par des « mutations » déclenchées par des « actions ». En 2019, Eduardo San Martin Morote, membre de l'équipe de Vue, écrit Pinia pour expérimenter une forme plus simple, pensée pour l'API de composition, sans mutations. L'expérience réussit : en 2022, Pinia devient la bibliothèque recommandée, et Vuex n'est plus maintenu que pour les anciens projets.

10.2 Un premier store

La démonstration code/ch10/demo-pinia/ réduit le problème à l'essentiel : deux composants frères, sans lien de parenté, qui partagent un panier. On ajoute Pinia au projet, puis on l'installe dans l'application comme le routeur :


$ npm install pinia@4.0.3

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

createApp(App).use(createPinia()).mount("#app");

Un store se déclare avec defineStore, dans un module du dossier src/stores/ :


import { ref, computed } from "vue";
import { defineStore } from "pinia";

export const usePanierStore = defineStore("panier", () => {
  // État : des ref
  const titres = ref([]);

  // Accesseurs (getters) : des computed
  const nombre = computed(() => titres.value.length);

  // Actions : des fonctions
  function ajouter(titre) {
    titres.value.push(titre);
  }
  function vider() {
    titres.value = [];
  }

  return { titres, nombre, ajouter, vider };
});
Proposition 10.2Store en style setup

defineStore(identifiant, setup) reçoit un identifiant unique et une fonction écrite comme le <script setup> d'un composant. Ce qu'elle renvoie forme le store :

  • chaque ref (ou reactive) renvoyée est de l'état ;
  • chaque computed est un accesseur (getter) ;
  • chaque fonction est une action.

Tout ce qui fait partie de l'état doit être renvoyé : une ref gardée privée échapperait aux outils de Pinia.

defineStore ne crée rien : il renvoie une fonction, usePanierStore, dont le nom suit la convention des composables. Le store est créé au premier appel, puis chaque appel suivant renvoie le même objet. Les deux composants l'appellent, chacun de son côté :


<script setup>
import { usePanierStore } from "../stores/panier.js";

const panier = usePanierStore();
const livres = ["Programmation Java", "Le Petit Prince", "L’Étranger"];
</script>

<template>
  <section>
    <h2>Catalogue</h2>
    <button
      v-for="titre in livres"
      :key="titre"
      type="button"
      @click="panier.ajouter(titre)"
    >
      Ajouter « {{ titre }} »
    </button>
  </section>
</template>

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →