Cours 10 - useEffect et Zustand
useEffect: montage, démontage, mise à jour d’une composante
Section intitulée « useEffect: montage, démontage, mise à jour d’une composante »On utilisera le hook useEffect pour contrôler le moment auquel certaines opérations sont effectuées dans une composante. Plutôt que d’être exécuté à chaque rendu, le code à l’intérieur d’un useEffect est exécuté selon des conditions précises. On utilse souvent ce hook pour exécuter des effets secondaires.
Les effets secondaires sont des opérations qui sortent des opérations normales d’une composantes (fonction pures). Une synchronisation avec un serveur, mise en cache de données ou la génération d’un log, ajout d’un écouteur d’événement, pourraient être de bons exemples d’effets secondaires.
Comme les autres hooks, useEffect ne peut qu’être utilisé à la racine de la composante (pas dans une fonction, un if, une boucle…). Le premier paramètre de useEffect est une fonction à appeler après chaque rendu.
import {useEffect} from 'react';const uneComposante = props => {
useEffect(() => { // Cette opération est exécutée après le premier rendu et les suivants (donc inutile) });};Si useEffect modifie le state, un nouveau rendu sera exécuté et une boucle infinie s’en suivra. On utilisera alors le deuxième paramètre de useEffect.
useEffect(() => { // Cette opération ne sera exécutée qu'au premier rendu}, []);Le 2e paramètre est un tableau de dépendances. Les variables qu’on y insérera seront évaluées à chaque rendu et si une valeur venait qu’à être modifiée, la fonction de useEffect serait éxécutée à nouveau.
const [nbEssaie, setNbEssais] = useState(0);
useEffect(() => { // Cette opération ne sera exécutée qu'au premier rendu et à chaque fois que // props.match.params.userId OU nbEssais changeront de valeur}, [props.match.params.userId, nbEssais]);Il est aussi possible de retourner une fonction depuis la fonction passée en paramètre. Celle-ci sera exécutée à la suppression de la composante. On pourrait ainsi utiliser un addEventListener dans le useEffect et retourner une fonction faisant un removeEventListener.
useEffect(() => { // Cette opération ne sera exécutée qu'au premier rendu
return () => { // Cette opération sera exécutée à la destruction (démontage) de la composante }}, []);Requête asynchrone
Section intitulée « Requête asynchrone »Bien que des librairies comme Axios font un travail efficace, nous utiliserons la méthode fetch pour interroger un serveur. Fetch n’utilise pas l’objet typique xmlHttpRequest (comme le fait axios). Son utilisation retourne une promesse afin de mieux enchaîner les différentes requêtes asynchrones et il est de plus compatible avec les Service Workers si l’envie de développer une Progressive web app (PWA) vous prenait.
//Requête en GETfetch('url').then(resp => resp.json) //la réponse "incomplète" à convertir (depuis un json ici).then(data => console.log(data)); //les données retournées par le serveur
//Requête en POSTfetch('url', { method: 'POST', headers: { 'Content-Type' : 'application/json'}, //dans la plupart des cas body: JSON.stringify({ //informations envoyées au serveur prop1: val1, prop2: val2 })}).then(resp => resp.json()).then(data => console.log(data));Async / Await
Section intitulée « Async / Await »On utilise la promesse retournée par fetch pour enchaîne par la suite l’exécution d’autres fonctions en utilisant sa méthode then. On peut donc y récupérer l’information, mais aussi retourner une nouvelle promesse pour continuer une séquence asynchrone. Les promesses permettent donc de garder le code ordonné et lisible sans se perdre dans une série de callbacks entrecroisés. D’autres solutions telles ques Rxjs ont aussi vu le jour, mais celle-ci ne sera pas abordée durant la session.
Avec l’avènement de ES2018, il est maintenant possible d’utiliser async/await en combinaison avec les promesses:
async () => { const resp = await fetch('url'); const data = await resp.json(); console.log(data);};Await fait une pause (en quelque sorte) dans le code en attendant que la promesse soit résolue. Il est impossible d’utiliser await si la fonction qui englobe cette commande n’est pas marquée par un async. Les deux méthodes then et async/await peuvent aussi être mélangées selon le style de programmeur.
async () => { const data = await fetch('url') .then(resp => resp.json()); console.log(data);};Si on désire exécuter un fetch après le premier rendu de la composante, on combinera l’utilisation de fetch et useEffect:
Fetch au chargement d’une composante client
Section intitulée « Fetch au chargement d’une composante client »useEffect(() => { // On ne peut pas déclarer la fonction de useEffect comme async parce que celle-ci retournerait une promesse (plutôt qu'une fonction à exécuter au démontage) const fetchData = async () => { const resp = await fetch('url'); const data = await data.json(); // on modifierait probablement le state de la composante ici }; fetchData();}, []);Ajouter du son
Section intitulée « Ajouter du son »L’une des méthodes pour faire jouer des sons dans une une application React est par l’utilisation de react-use-audio-player (une version React de Howler.js).
Installation
Section intitulée « Installation » npm install react-use-audio-playerDans le navigateur, la lecture d’un son est soumise à la politique de la lecture automatique des médias et nécessite donc que l’utilisateur en donne son accord ou interagisse avec la page. C’est pour cette raison que seules les vidéos en sourdines peuvent être jouées par défaut. L’une des méthodes pour démarrer la lecture automatiquement est donc souvent de mettre un bouton jouer en superposition ou un bouton pour faire basculer le son sourdine/lecture.
Version simple
Section intitulée « Version simple »Dans une composante client, on récupère les différentes propriétés et méthodes de useAudioPlayer après lui avoir passé le chemin vers le son à jouer:
'use client';import { useAudioPlayer } from "react-use-audio-player";const Composante = () => { const {play} = useAudioPlayer('/chemin/vers/le/son', {/* options */});
const onClickHandler = () => { play(); };
return ( <button onClick={onClickHandler}>Jouer</button> );};Il peut arriver qu’il soit nécessaire de renommer les méthodes/propriétés si on désire par exemple jouer deux sons dans la même page:
'use client';import { useAudioPlayer } from "react-use-audio-player";const Composante = () => { const {play: playSound1} = useAudioPlayer('/chemin/vers/le/son1', {/* options */}); const {play: playSound2} = useAudioPlayer('/chemin/vers/le/son2', {/* options */});
const onClickHandler = () => { playSound1(); };
const onOtherClickHandler = () => { playSound2(); };
return ( <> <button onClick={onClickHandler}>Jouer son 1</button> <button onClick={onOtherClickHandler}>Jouer son 2</button> </> );};Faire jouer plusieurs fois le même son (à haute vélocité)
Section intitulée « Faire jouer plusieurs fois le même son (à haute vélocité) »L’idée est d’utiliser plusieurs canaux en simultanée. On se chargerait aussi de précharger le son:
'use client';import { useAudioPlayer } from "react-use-audio-player";const Composante = () => { const { load } = useAudioPlayer();
useEffect(() => { // préchargement load('/chemin/vers/le/son', { autoplay: false }); }, []);
const onClickHandler = () => { load('/chemin/vers/le/son', { autoplay: true }); };
return ( <button onClick={onClickHandler}>Jouer</button> );};Faire jouer un son (musique) qui persiste aux changements de page
Section intitulée « Faire jouer un son (musique) qui persiste aux changements de page »Il faut utiliser une instance partagée de l’AudioPlayer pouvant être accessible sur plusieurs pages.
-
Il faut débuter par emballer les pages du
AudioPlayerProvider. Comme celui-ci doit être utilisé dans des composantes client seulement, il faut souvent créer une composante client qui s’occupe de l’utiliser par composition:'use client';import { AudioPlayerProvider } from 'react-use-audio-player';const AudioPlayerWrapper = ({children}) => {return <AudioPlayerProvider>{children}</AudioPlayerProvider>;}; -
On utilisera ensuite cette nouvelle composante comme parent des pages à venir (donc souvent dans un fichier
layout.jsx). Toutes les pages ayant cette composante comme parent pourront donc partager la même instance d’AudioPlayer.{ /* reste de la composante */}<body><AudioPlayerWrapper>{children}</AudioPlayerWrapper></body> -
Par la suite, plutôt que d’utiliser la fonction (le hook)
useAudioPlayer, on utilserauseAudioPlayerContext.const Page1 = () => {const { play, load } = useAudioPlayerContext();useEffect(() => {load('/chemin/vers/le/son', {autoplay: true});}, []);return <>{/* contenu de la page */}</>};const Page2 = () => {const { setVolume } = useAudioPlayerContext('/chemin/vers/le/son');useEffect(() => {setVolume(.25);}, []);return <>{/* contenu de la page */}</>};
Développé par Poimandres, Zustand permet de déclarer un état global à l’application (ou plusieurs). Il est ensuite possible de consommer cet état dans n’importe quelle composante client de l’application sans reposer sur le props drilling. React compte aussi sa version native nommée Context. Les deux sont utilisables en parallèle ou en combinaison au besoin.
Contrairement au Context, seules les composantes utilisant la propriété mise à jour effectueront un nouveau rendu. Une modification de n’importe quelle propriété de React Context entraîne un nouveau rendu de toutes les compoosantes qu’il contient.
Installation
Section intitulée « Installation » npm install zustandCréation d’un store
Section intitulée « Création d’un store »On crée un store pour regrouper les propriétés et méthodes relative à un service supplémentaire à ajouter à l’application. On pourrait par exemple en créer un pour la gestion du thème affiché dans l’application et pour les fonctionnalités relatives à l’authentification du côté client.
Dans un dossier _stores, on crée donc un fichier par service.
Répertoiresrc/
Répertoireapp/
Répertoire_stores/
- themeStore.js
En utilisant la fonction create, on crée donc un objet contenant les propriétés et méthodes nécessaires au fonctionnement du service. Le store créé sera utilisé comme un hook React et portera donc le préfixe use:
Propriétés
Section intitulée « Propriétés »import { create } from 'zustand';
const useTheme = create((set) => ({ // propriétés currentTheme: 'dark', themes: ['dark', 'light', 'contrast'], currentThemeIndex: 0,
// méthodes à venir}));Méthodes
Section intitulée « Méthodes »Pour les méthodes devant modifier l’une des propriétés du store (c’est le cas de la plupart des méthodes), on utilise la fonction set récupérée en paramètre. On lui passe un objet ne contenant que les propriétés à modifier et leur nouvelle valeur:
import { create } from 'zustand';
const useTheme = create((set) => ({ // propriétés currentTheme: 'dark', themes: ['dark', 'light', 'contrast'], currentThemeIndex: 0,
// méthodes setTheme: (selection, index) => set({currentTheme: selection, currentThemeIndex: index})}));Si les valeurs actuelles du store sont nécessaires pour obtenir les nouvelles valeurs, on passera plutôt une fonction à set:
import { create } from 'zustand';
const useTheme = create((set) => ({ // propriétés currentTheme: 'dark', themes: ['dark', 'light', 'contrast'], currentThemeIndex: 0,
// méthodes setTheme: (selection) => set((state) => ({ currentTheme: selection, currentThemeIndex: state.themes.findIndex((theme) => theme === selection) ?? -1 })), nextTheme: () => set((state) => { const index = (state.currentThemeIndex + 1) % state.themes.length; return {currentTheme: state.themes[index], currentThemeIndex: index}; })}));Utilisation du store
Section intitulée « Utilisation du store »Il reste ensuite à consommer le store en créant un sélecteur pour chaque propriété ou méthode nécessaire à l’utilisation d’une composante (client):
const MaComposante = () => { // on récupère chaque tranche une à la fois const currentTheme = useTheme(state => state.currentTheme); const nextTheme = useTheme(state => state.nextTheme);
return ( <div> <p>Thème actif: {currentTheme}</p> <button onClick={nextTheme}>Thème suivant</button> </div );};Extension - persist
Section intitulée « Extension - persist »Zustand permet aussi de sauvegarder l’état du store dans le localStorage afin d’assurer une persistence des données. Il faut pour cela transformer la création du store en utilisant l’extension persist.
On devra également:
- donner une nom à la propriété du localStorage sous laquelle on veut sauvegarder l’état
- sélectionner les propriétés à conserver plutôt que de tout sauvegarder (partialize)
import { create } from 'zustand';import { persist, createJSONStorage } from 'zustand/middleware';
const useTheme = create()(persist((set) => ({ // propriétés currentTheme: 'dark', themes: ['dark', 'light', 'contrast'], currentThemeIndex: 0,
// méthodes setTheme: (selection) => set((state) => ({ currentTheme: selection, currentThemeIndex: state.themes.findIndex((theme) => theme === selection) ?? -1 })), nextTheme: () => set((state) => { const index = (state.currentThemeIndex + 1) % state.themes.length; return {currentTheme: state.themes[index], currentThemeIndex: index}; })})), { name: 'nom-dans-localstorage', storage: createJSONStorage(() => sessionStorage), partialize: (state) => ({currentTheme: state.currentTheme, currentThemeIndex: state.currentT})});