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.
- Dans un composant une
refdu<script setup>: le texte d'un champ, l'ouverture d'un menu. Elle naît et meurt avec le composant. - Chez un parent qui la transmet par props et reçoit les modifications par événements (chapitre 5). C'est le bon choix tant que la donnée ne descend que d'un ou deux niveaux.
- Fournie par un ancêtre avec
provideetinject(chapitre 7) : elle traverse tous les niveaux, mais ne vit pas plus longtemps que l'ancêtre qui la fournit. - Dans un store : un objet réactif unique pour toute l'application, créé à sa première utilisation et conservé ensuite, quels que soient les composants affichés.
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 };
});
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(oureactive) renvoyée est de l'état ; - chaque
computedest 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 coursEn accès libre, sans inscription.