Pinia 🍍 : l’avenir du state management Vue.js en 10 minutes
Il y a quelques mois nous avons eu la chance d’assister à la plus grande et la plus importante conférence Vue.js du monde : Vue.js Amsterdam 🙌🏻
Nous avons été enthousiasmés par tout ce que nous y avons vu (sans mauvais jeu de mot), découvert et appris. En particulier, sur Pinia, une bibliothèque de gestion d’état pour Vue.js, qui a suscité notre intérêt et notre motivation à écrire un article de blog à ce sujet.
“Bon…” vous me direz, elle bien jolie ton histoire, mais c’est quoi Pinia exactement ?
Moins de contexte et plus de dev ? Vous pouvez directement vous dirigez sur la partie 👉Installer Pinia sur votre projet 🚀.
Dans cet article nous nous concentrerons sur l’utilisation de Pinia au sein d’une application Vue.js en version 3 (la fin du support de la version 2 de Vue.js étant annoncé pour le 31 décembre 2023).
Pinia c’est quoi ? 🤔
Et si on commençait par le commencement. On en a un peu parlé dans l’introduction mais un peu plus de détail ne fera pas de mal 😁
Pinia est une librairie de gestion d’état pour Vue.js. Elle permet de centraliser des états appelés states (ce sont des variables contenant les données de votre application) en un même endroit appelé store.
Ce store agit comme un conteneur accessible partout dans l’application (pages, composants, composable, …) et tout le monde peut lire (par le biais du state ou de getters) et modifier (par le biais d’actions) son contenu. Pas de panique, ces notions seront détaillées par la suite 😉
La gestion de l’état d’une application Vue.js peut devenir complexe du fait des multiples parties d’état réparties à travers divers composants et de leurs interactions. C’est ainsi que des librairies de gestion d’état comme Pinia ont pu voir le jour 🙋♀️
Maintenant qu’on sait ce qu’est Pinia, vous devez sûrement vous demander pourquoi Pinia et pas une autre librairie de gestion d’état (vous avez peut-être entendu parler de Vuex par exemple) ? Ça tombe bien, c’est la prochaine partie de cet article.
Pourquoi Pinia ? 🧐
Pinia se concentre sur la simplicité et la facilité d’utilisation, en offrant une syntaxe concise et facile à comprendre pour définir l’état de l’application et les mutations. Il est également facile de l’intégrer avec d’autres bibliothèques et outils Vue.js.
Si vous êtes un développeur Vue.js à la recherche d’une alternative à Vuex, qui soit légère et facile à utiliser, Pinia pourrait être la solution que vous recherchez.
Rien de mieux qu’une bonne vieille checklist pour justifier l’utilisation de quelque chose 😉
👉 Syntaxe concise
👉 Facile à prendre en main
👉 Compatible Vue2 et Vue3
👉 Des APIs de types Composition-API
👉 Une solide prise en charge de l’inférence de type lorsqu’elle est utilisée avec TypeScript
👉 Autocomplétion pour le JS
👉 Fonctionne avec Vue Devtools (on y reviendra 😉)
👉 Facile à intégrer avec d’autres bibliothèques et outils Vue.js.
👉 Hot Module Replacement
👉 Recommandée par la communauté 🙌
👉 Proposée dans la CLI pour initier un nouveau projet avec npm init vue@latest
En bref, Pinia offre une grande expérience développeur en se concentrant sur la simplicité et la facilité d’utilisation tout en offrant une grande expérience développeur.
“Une API plus simple avec moins de fioritures”
“Une solide prise en charge de l’inférence de type lorsqu’elle est utilisée avec TypeScript”
“Les utilisateurs actuels connaissent peut-être Vuex, l’ancienne bibliothèque officielle de gestion d’état pour Vue.js. Pinia jouant le même rôle dans l’écosystème, Vuex est désormais en mode maintenance. Elle fonctionne toujours, mais ne proposera plus de nouvelles fonctionnalités. Il est recommandé d’utiliser Pinia pour les nouvelles applications.”
(Source : documentation officielle vuejs.org)
Cette recommandation est d’ailleurs de plus en plus visible dans la communauté Vue.js. À titre d’exemple, lors des conférences à Amsterdam, nous avons entendu parler de Pinia, mais pas de Vuex. Pinia est maintenant la librairie officiellement recommandée pour la gestion d’état d’une application VueJS.
Pourquoi pas directement avec Vue.js ?
Aujourd’hui il est possible de créer au sein de son application un state global réactif partagé (un store) sans dépendances externes. On retrouve principalement deux manières de faire.
Une première approche serait de partager nos informations via un passage de props en profondeur (ou props drilling en anglais). L’idée est d’injecter nos props dans tous nos composants, dans toute l’application.
Un exemple en image :
(Source : https://vuejs.org/guide/components/provide-inject.html#prop-drilling)
Petit problème avec cette solution, nos props non utilisées vont se retrouver à transiter un peu partout dans des composants qui n’en n’ont pas l’utilité. Sur une petite application, pourquoi pas…Dans une application plus grande, il sera facile de s’y perdre.
La seconde approche est l’utilisation de provide/inject. La documentation officielle de Vue.js résume celle-ci de la manière suivante :
“Un composant parent peut servir de fournisseur de dépendances pour tous ses descendants. Tout composant enfant de l’arborescence, quelle que soit sa profondeur, peut injecter les dépendances fournies par des composants présents dans sa chaîne de composants parents.”
(Source : https://vuejs.org/guide/components/provide-inject.html#prop-drilling)
(Source : https://vuejs.org/guide/components/provide-inject.html#prop-drilling)
Pour fournir des données aux descendants d’un composant, utilisez l’option provide :
import { provide } from 'vue'
export default {
setup() {
provide(/* key */ 'message', /* value */ 'hello!')
}
}
Pour récupérer les données fournies dans un provider, il faudra utiliser la fonction inject dans un composant enfant :
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
Ici, le problème principal sera le même qu’avec le props drilling : la maintenabilité. Il pourra être compliqué de traquer les modifications apportées à une variable de notre état partagé. De plus, il faudra toujours faire attention à se trouver dans un composant sous le provider.
“Bah alors, pourquoi je m’embêterais à utiliser une solution comme Pinia ?”
Très bonne question, tu es attentif·ve ! 👏
Pour cela, nous reprendrons simplement quelques exemples tirés de la documentation Vue.js qui sont assez parlants
“Si notre solution de gestion d’état à la main suffit dans les scénarios simples, il y a beaucoup plus d’éléments à prendre en compte dans les applications de production à grande échelle :
- Des conventions plus solides pour la collaboration en équipe
- L’intégration avec les Vue Devtools, y compris la timeline, l’inspection des composants et le débogage par voyage dans le temps.
- Hot Module Reload
- Prise en charge du rendu côté serveur”
(Source : https://fr.vuejs.org/guide/scaling-up/state-management.html#pinia)
Bon, je ne sais pas ce que tu en dis, mais la théorie, moi, ça va 5 minutes, il est temps de mettre les mains dans le cambouis 👩🏭🧑🏭
Installer Pinia sur votre projet 🚀
Tout d‘abord, ajoutez la dépendance sur votre projet.
yarn add pinia
// or
npm install pinia
Ensuite il faut créer une instance Pinia et la déclarer à votre application Vue.js.
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
Désormais vous pouvez utiliser Pinia et créer votre premier store ! 🥳
Vue.js Devtools
Avant de démarrer le développement de notre store, il est vivement conseillé d’installer l’extension Vue Devtools sur votre navigateur (conçu par le créateur de Vue.js, Evan You).
Cette extension vous permet de visualiser l’état de votre store et de le manipuler directement depuis votre navigateur. Une fois installée, vous pourrez y accéder dans le panel de développeurs lorsque vous serez sur votre application en mode DEV.
💡 Il est parfois nécessaire d’ajouter la ligne suivante dans le fichier main.ts pour l’activer: Vue.config.devtools=true |
L’outil propose plusieurs fonctionnalités utiles lorsque l’on développe une application Vue.js (avec ou sans stores). Dans le cas de notre store Pinia, on peut constater que le store est automatiquement détecté et que nos données sont visibles et éditables à tout instant, offrant ainsi la possibilité de tester dynamiquement nos pages/vues/composants directement depuis le navigateur.
Par exemple, dans le cas suivant il est tout à fait possible d’ajouter de nouveaux items à notre tableau “parking” et de vérifier si les nouveaux éléments s’affichent correctement sur la page.
Créer son premier store ! 🛒
Pour comprendre le fonctionnement d’un store sur une application Vue.js, prenons un exemple assez simple: gérer les données d’une application de réservation de places de parking.
L’objectif étant de pouvoir lister les places, indiquer si une place de parking est disponible et pouvoir réserver ou libérer une place.
Le code de l’application est disponible à l’adresse suivante 👉 https://github.com/Zenika/pinia-example
Comment modéliser un store ?
Dans votre application Vue.js, vous avez indiqué à votre application que Pinia a été installé, mais cela ne veut pas dire pour autant qu’un store a été initialisé.
Un store est tout simplement un fichier stocké dans src/stores où on va appeler la fonction defineStore en lui passant en paramètre un nom de store unique (qui correspond au storeId). Dans cet exemple nous voulons modéliser un parking, nous l’appellerons donc parking et parkingStore.
import { defineStore } from 'pinia'
export const parkingStore = defineStore('parking', {})
Et voilà, nous avons notre premier store ! – Quoi c’est tout ?-, et bien oui, nous avons créé un store pour notre parking, mais il nous manque évidemment quelques éléments… 😉
Introduction aux states
La seconde étape consiste à modéliser les places de parking, qui seront les données manipulées par notre application.
Les places se traduisent de la façon suivante :
export type ParkingPlace = {
id: number,
isAvailable: boolean
}
Le parking se définit par une liste de places, comme ceci :
const parking: Ref<ParkingPlace[]> = ref([
{id:1, isAvailable: true},
{id:2, isAvailable: true},
{id:3, isAvailable: false}
])
Maintenant que nous avons modélisé en Typescript ce qu’est un parking et une place de parking, construisons notre parking dans notre store.
Dans un store, les données sont appelées “states”, cela peut prendre la forme d’objet javascript, de nombre, de chaînes de caractères etc.
Ici, nous souhaitons utiliser notre parking en lui attribuant un nombre de places numérotées, regardons ensemble comment le faire avec Pinia :
export const useParkingStore = defineStore('parking', () => {
const parking: Ref<ParkingPlace[]> = ref([
{id:1, isAvailable: true},
{id:2, isAvailable: true},
{id:3, isAvailable: false}
])
return { parking }
})
Nous retrouvons notre propriété “state” qui contient les données de notre store, ici elle contient uniquement un tableau de places qui correspond à notre parking, mais vous pouvez y ajouter d’autres données selon le besoin.
Le store défini ci-dessus est déjà fonctionnel, puisque vous pouvez accéder à votre parking depuis un composant Vue, comme démontré ci-dessous :
<script setup lang="ts">
import { useParkingStore } from '../stores/parking';
const store = useParkingStore()
</script>
<template>
<div>
<div v-for="parkingPlace in store.parking" :key="parkingPlace.id">
<div>{{ parkingPlace.id }}</div>
<div>{{ parkingPlace.isAvailable }}</div>
</div>
</div>
</template>
Pour interagir avec ce conteneur, il y a trois concepts clés à retenir : state, getters et actions.
Introduction aux getters
Grâce aux states nous avons enfin notre parking, désormais nous souhaitons indiquer à notre utilisateur les places encore disponibles, pour cela nous allons faire un getter.
Un getter est une fonction où on va lire nos données et parfois les filtrer, dans notre cas on veut récupérer les places de parking disponibles et réservées, soit celles qui ont la propriété isAvailable === true et isAvailable === false. Pour cela, on va créer deux getters qui s’appellent “availableParkingPlaces” et “reservedParkingPlaces”, qui récupèrent le parking stocké dans notre state et filtre ses éléments.
const availableParkingPlaces = computed(() => parking.value.filter(place => place.isAvailable))
const reservedParkingPlaces = computed(() => parking.value.filter(place => !place.isAvailable))
Maintenant que les getters sont définis dans notre store, utilisons-les dans notre composant Vue pour afficher la liste des places de parking disponibles et réservées :
<script setup lang="ts">
import { useParkingStore } from '../stores/parking';
const store = useParkingStore()
</script>
<template>
<div class="parking-list">
<h2>Reserved Parking Places</h2>
<div v-for="parkingPlace in store.reservedParkingPlaces" :key="parkingPlace.id">
<div>{{ parkingPlace.id }}</div>
</div>
<h2>Available Parking Places</h2>
<div v-for="parkingPlace in store.availableParkingPlaces" :key="parkingPlace.id">
<div>{{ parkingPlace.id }}</div>
</div>
</div>
</template>
Introduction aux actions
C’est super, notre application commence vraiment à prendre forme, mais il reste encore une dernière petite étape : Réserver ou libérer une place de parking ! Et oui, notre parking pour le moment existe mais n’est pas très dynamique… Pour cela nous allons créer des actions !
Les actions sont des fonctions qui vont nous permettre de modifier nos states, dans notre cas, la disponibilité de nos places de parking.
Nous allons donc créer deux fonctions “reserveParkingPlace” et “freeParkingPlace”.
export const useParkingStore = defineStore('parking', () => {
...
function reserveParkingPlace(id: ParkingPlace['id']) {
const indexOfParkingPlace = parking.value.findIndex((place) => place.id === id)
if(indexOfParkingPlace >= 0) parking.value[indexOfParkingPlace].isAvailable = false
}
function freeParkingPlace(id: ParkingPlace['id']) {
const indexOfParkingPlace = parking.value.findIndex((place) => place.id === id)
if(indexOfParkingPlace >= 0) parking.value[indexOfParkingPlace].isAvailable = true
}
return {..., reserveParkingPlace, freeParkingPlace }
})
Nos actions sont prêtes, utilisons-les dans notre composant pour interagir avec les listes libres et réservées. Pour cela il nous suffit de créer des boutons qui appelleront nos actions du store au moment de l’évènement @click.
<script setup lang="ts">
import { useParkingStore } from '../stores/parking';
const store = useParkingStore()
</script>
<template>
<div>
<h2>Reserved Parking Places</h2>
<div v-for="parkingPlace in store.reservedParkingPlaces" :key="parkingPlace.id">
<div>{{ parkingPlace.id }}</div>
<button @click="store.freeParkingPlace(parkingPlace.id)">
Free place
</button>
</div>
<br />
<h2>Available Parking Places</h2>
<div v-for="parkingPlace in store.availableParkingPlaces" :key="parkingPlace.id">
<div>{{ parkingPlace.id }}</div>
<button @click="store.reserveParkingPlace(parkingPlace.id)">
Reserve place
</button>
</div>
</div>
</template>
Et voilà, notre store est complet et prêt à être utilisé 🎉
Vous avez maintenant toutes les cartes en main pour démarrer rapidement avec Pinia 💪
Merci de nous avoir lu, nous espérons que cet article vous aura plu.
On vous dit à bientôt pour de nouvelles aventures 😃