Blog Zenika

#CodeTheWorld

Web

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 😃

Auteurs/autrices

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.