Gérer le cycle de vie des composants
Les hooks et la boucle de rendu en React
Dans les chapitres précédents, nous avons exploré la création de composants d’affichage et leur stylisation. Ces derniers se limitaient à présenter du contenu HTML variant selon leurs props, sans gestion interne de données. Ils étaient ce qu’on appelle des stateless components (composants sans états). Pour gérer des états et ainsi gérer toutes sortes d’interactions avec l’utilisateur (récupérer des données API, produire des animations, etc.), il manque alors deux concepts fondamentaux au développement en React : d’une part, comprendre le cycle de vie d’un composant, d’autre part, comprendre comment s’y connecter pour faire persister des données entre deux cycles de rendu.
L’un engendrant l’autre, commençons par comprendre comment se connecter au cycle de rendu. Le nœud entre ces deux notions est le concept de hook (« crochet »). Un hook est, comme son nom anglais l’indique, une fonction qui permet d’utiliser les fonctionnalités de React en se connectant (ou en « s’accrochant ») au cycle de vie d’un composant. Conventionnellement, tous les hooks commencent par le mot-clé use suivi d’un ou de plusieurs mots en PascalCase, par exemple :...
La gestion des états
1. Le hook useState<T>
a. Le fonctionnement de useState<T>
Le hook useState<T> est le premier hook que nous allons étudier. Il est, par ailleurs, le hook le plus fréquemment utilisé en React, puisqu’il équivaut à une opération de déclaration de variable connectée au cycle de rendu. useState<T> prend une valeur optionnelle en paramètre. En retour, la fonction renvoie un tuple (un tableau) contenant deux éléments que l’on récupère généralement en utilisant l’opérateur de déstructuration. Le premier élément du tableau est la valeur stockée dans le hook, le second élément est une fonction de mutation. Cette fonction, lorsqu’elle est appelée, d’une part, met à jour la valeur stockée et, d’autre part, son effet de bord est d’enclencher un re-rendu du composant pour que la nouvelle valeur soit correctement affichée. C’est pour cela qu’il n’est pas possible de passer par une déclaration de variable JavaScript pour stocker des données : c’est à partir de l’appel de cette fonction, et seulement via cette fonction, qu’un re-rendu peut être déclenché. En général, pour indiquer le côté immuable des deux éléments du tableau retourné, la déstructuration ne se fait pas avec le mot-clé let, mais avec le mot-clé const. Enfin, bien qu’il soit possible de donner n’importe quel nom aux deux éléments du tableau déstructuré, par convention la fonction de mutation reprend le nom de la variable précédé de set. Par exemple, pour une variable count, on obtiendrait la fonction de mutation setCount.
Le hook useState<T> prend aussi un type générique pour indiquer quel type de valeur doit être manipulé. Renseigner le type explicitement est toutefois facultatif, le compilateur TypeScript ayant la capacité d’inférer lui-même le type passé en paramètre.
Dans certains cas, la précision du type peut être importante pour lever l’ambiguïté. Par exemple dans le cas où l’on n’a...
La gestion des références avec useRef<T>
1. Qu’est-ce qu’une référence ?
Le hook useRef<T> est en quelque sorte le frère jumeau et l’opposé du hook useState<T>. Il s’agit d’un hook qui permet de créer des références mutables qui persistent, comme les valeurs de retour de useState<T>, durant tout le cycle de vie du composant. Leur principale similitude réside donc dans la persistance des données. Leur principale différence, c’est que useState<T> retourne des valeurs immuables (d’où la nécessité de passer par une fonction de mutation). Le hook useRef<T>, lui, n’a pas besoin de retourner un couple valeur/fonction ; il ne renvoie que la référence vers la valeur mutable. La conséquence est que la mutation de la valeur pendant le cycle de vie du composant ne provoque pas de re-rendu de ce dernier. En effet, pour le hook useState<T>, c’est la fonction de mutation qui permet un tel re-rendu.
Son objectif principal est d’offrir un moyen de stocker des valeurs qui ne sont pas écrasées entre deux re-rendus, mais qui, lorsqu’elles sont mises à jour, n’ont pas besoin de déclencher le re-rendu du composant.
Le hook useRef<T> prend un paramètre, qui correspond à la valeur initiale de la référence. Le type générique peut éventuellement être renseigné. Ce sera surtout nécessaire lorsqu’aucune valeur initiale n’est renseignée en paramètre. Toutefois, l’argument de useRef<T> n’est pas facultatif. Dans le cas où l’on ne souhaite pas de valeur initiale, le plus courant est de renseigner la valeur null.
Ce hook retourne un objet de type RefObject<T>. Cet objet ne dispose que d’une propriété : current, de type T. À titre d’exemple, un appel à useRef<number>(null) retournera un objet de type RefObject<number> dont la propriété current sera de type number. C’est cette propriété que l’on utilise pour mettre directement à jour la valeur de la référence mutable.
import { getRandomNumber } from "./utils/get-random-number"; ...La gestion du cycle de vie avec useEffect
1. Le principe de useEffect
À l’exception du code des hooks, tout le code d’un composant sera exécuté de nouveau à chaque re-rendu. Mais il arrive souvent que ce ne soit pas un effet désiré. Certaines parties du code peuvent n’avoir besoin de s’exécuter qu’à certains moments précis.
C’est là qu’intervient le hook useEffect. useEffect est un hook qui gère les effets secondaires dans les composants. Un effet secondaire (ou effet de bord) correspond à n’importe quelle action pouvant se produire en dehors du processus de rendu normal : récupération des données, manipulation directe du DOM, configuration d’abonnements (subscribers). Le nom du hook est explicite : il gère le code ayant un « effet » sur l’environnement du composant après que celui-ci ait été « utilisé » (rendu). En somme, ce hook permet de déclencher l’exécution de morceaux de code à des moments définis plutôt qu’à chaque re-rendu.
Dans le cas des composants de classe, les hooks n’existent pas. Les effets sont gérés de manière limitée. Lorsqu’on hérite de la classe React.Component<T, U> pour créer un composant de classe, il est possible de surcharger les fonctions de la classe parente : componentDidMount (effet qui ne s’exécute qu’au lancement du composant, par exemple pour récupérer des données), componentDidUpdate (effet qui s’exécute lorsque les états du composant se mettent à jour), componentWillUnmount (effet qui ne s’exécute que lorsque le composant est sur le point d’être retiré de l’arborescence des composants).
Avec les composants fonctionnels, tous ces comportements peuvent être reproduits en utilisant un seul et même hook : useEffect.
2. Le hook useEffect sans tableau de dépendances
Le hook useEffect prend deux paramètres : une fonction de callback, qui sera appelée au moment voulu, et un tableau de dépendances. C’est dans le tableau de dépendances que se concentre la complexité réelle...
La séparation du code avec des hooks personnalisés
Nous avons appris à utiliser des hooks natifs de React. Néanmoins, il est commun de créer ses propres hooks personnalisés. Imaginons par exemple que plusieurs composants intègrent un système de chronomètre tel que celui créé dans la section précédente. L’on pourrait être tenté de dupliquer la logique en la copiant-collant dans chaque composant. Cependant, comme dans beaucoup de langages de programmation, la redondance du code est à éviter - il n’est pas le lieu d’expliquer ici en quoi copier et coller son code à plusieurs endroits est une très mauvaise pratique.
Pour éviter ce type de redondances en React, il est possible de créer des hooks personnalisés. Dans un premier temps, il faut savoir que les hooks personnalisés se placent le plus souvent dans un répertoire hooks/ à la racine du projet. Il faut garder en tête qu’il s’agit d’un cas général et que chaque équipe de développement peut être amenée à avoir des architectures de fichiers plus complexes au fur et à mesure que le projet grossit, et donc différentes d’un projet à l’autre.
Revenons à notre gestion du temps. Créons un fichier use-timer.tsx dans...
Les contextes globaux
1. La Context API
Il arrive souvent qu’une prop traverse tout l’arbre des composants et se retrouve dans plusieurs composants simplement pour « transiter » vers un composant de destination. Cette pratique s’appelle du property drilling (forage de propriétés). Elle est la conséquence de la structure même de React qui n’autorise qu’un binding unidirectionnel des propriétés « de haut en bas », ce qui amène souvent à passer des propriétés de composants en composants jusqu’à une certaine profondeur.
Cette redondance peut être évitée par l’usage du hook useContext<T>. Pour l’instant, la seule manière de faire passer des données entre composants était l’utilisation des props. Un contexte en React est un espace au sein duquel des variables sont partagées, ces variables étant accessibles depuis n’importe quelle partie de l’arborescence placée dans le fournisseur de contexte (provider). Tout ou partie d’une application en React peut être entourée d’un fournisseur de contexte.
La fonction pour créer un contexte est createContext<T>. Comme useState<T>, elle prend en paramètre un état initial de type T, qui peut être explicité ou inféré dynamiquement par le compilateur TypeScript. Il n’est pas toujours possible de savoir à l’avance quel sera l’état initial d’un contexte (celui-ci peut venir d’une API par exemple). Dans ce cas, on renseigne null par convention en paramètre par défaut. Néanmoins, utiliser null force à typer explicitement createContext<T>. La fonction createContext<T> renvoie deux composants : un fournisseur de contexte (provider) et un consommateur de contexte (consumer). Son retour est donc à stocker dans une constante exportée qui aura pour convention de nommage des composants la PascalCase. Une fois le contexte créé, le fournisseur de contexte peut désormais être utilisé en entourant une portion ou la totalité du code de l’application de ContextName.Provider. Pour mettre à jour la valeur contenue dans le contexte...