Comprendre le cycle de vie de ReactJS

Chaque composant React passe par différentes étapes durant lesquelles il est possible d'intervenir. On appelle ça le cycle de vie d'un composant. Celui-ci se découpe en 3 parties :

  • Mount : le montage. Il intervient quand une instance du composant est créé dans le DOM.
  • Update : la mise à jour. Ce cycle de vie est déclenché par un changement d'état du composant.
  • Unmount : le démontage. Cette méthode est appelée une fois qu'un composant est retiré du DOM.

Tout s'articule autour de ces 3 cycles de vies.

Les différentes évènements

Avant de voir en détail chacun des cycles de vie, voici les différentes étapes qui peuvent exister. En fonction du cycle de vie en cours, elles ne sont pas toutes appelées. Je ne vais pas parler des méthodes UNSAFE_ qui sont dépréciés et devraient disparaîtres dans React 17.

Mount

  1. constructor()
  2. render()
  3. componentDidMount()

Update

  1. shouldComponentUpdate(nextProps, nextState, nextContext)
  2. render()
  3. componentDidUpdate()

Unmount

  1. componentWillUnmount()

Toutes ces méthodes sont disponibles lorsque vous utilisez un class component. Pour intervenir sur le cycle de vie d'un functional component, nous verrons l'utilisation des hooks. En attendant, commencons par étudier ce cycle de vie qui va nous permettre d'isoler et de comprendre chaque partie.

Mount - Le montage

constructor()

Comme dans la programmation objet, le constructeur est ce qui est appelé en premier. Il intervient dès que le composant doit apparaître dans le DOM virtuel. Votre constructeur de class reçoit en premier paramètre ses props. Si vous l'implémentez, il sera important de bien appeler votre class parente avec le mot-clé super afin de lui fournir les props.

C'est au niveau de votre constructeur que vous allez initialiser l'état de votre composant (le state). Il vous arrivera également de venir .bind() certaines méthodes pour garantir le this de certaines méthodes.

Attention, durant cet évènement, les éléments du DOM n'existent pas! Vous ne pourrez pas sélectionner un élément en utilisant document.querySelector par exemple.

render()

Cette méthode est celle du rendu. Nous l'avons déjà vu dans un cours précédent. Elle intervient pour rendre votre JSX dans le DOM virtuel (et donc générer le HTML). C'est à ce moment là que vous avez l'état de votre composant à jour. Que ce soit vos props ou votre state, vos données sont disponibles et prêtes à être manipulées afin de rendre ce que vous souhaitez.

componentDidMount()

Enfin, dès que le composant a fini de se monter, cette méthode s'exécute. Le DOM est correctement chargé et cette fonction devient intéressante pour, par exemple, tenter de manipuler les éléments du DOM.

Nous allons voir le fil de l'exécution avec l'exemple ci-dessous en y ajoutant quelques console.log

Exemple avec un class component

On aperçoit l'ordre d'exécution des différents éléments

Testons la sélection d'un élément du DOM

Bien que je n'utilise que très rarement la sélection d'un élément du DOM, il peut vous arriver d'en avoir besoin notamment lors de l'utilisation d'une librairie tierce.

Pour cela, je vous propose d'observer un document.querySelector durant l'exécution de chaque méthode.

2 null et 1 seul résultat 'positif'

Etonné ? En ce qui concerne le constructor, cela paraît évident puisque le DOM n'existe pas. De même pour le componentDidMount, il intervient après le rendu du composant qui s'occupe de créer les différents éléments du DOM. Cependant, on pourrait se demander pourquoi la méthode render obtient un null. Tout simplement car nous sommes dans le render qui correspond au montage du composant. Cela signifie que le DOM qui se trouve dans le retour de la méthode n'existe pas encore alors que mon instruction se situe avant le retour de la fonction : le DOM n'existe donc pas encore.

Exemple avec un functional component

Un functional component n'a pas accès aux méthodes de class de ce type. Cependant, il serait illogique de pouvoir continuer de faire du React convenablement sans pouvoir intervenir sur les différentes étapes du cycle de vie d'un élément en utilisant un functional component. Comme on a déjà pu le voir rapidement avec useState, nous allons devoir utiliser un nouveau hook React : useEffect.

Il existe un cours spécifique sur les hooks. Dans un premier temps, nous nous attardons à observer les différences entre une class et un functional component et c'est le cours sur les hooks qui rentrera dans le détail des hooks.

Observons attentivement le résultat des console.log. Tout d'abord, nous avons le premier qui s'exécute avant la déclaration du hook. Je déclare ensuite mon useEffect, sur lequel nous allons revenir, avec un console.log dedans en tentant de sélectionner l'élément du DOM #item-1. Enfin, après sa déclaration, j'ai deux console.log dont un qui tente également de sélectionner le même élément du DOM.

Dans le cas du cycle de vie du montage, le console.log qui tente de récupérer l'élément du DOM en dehors du useEffect ne va rien avoir comme résultat : null. Nous sommes dans le cas où le DOM virtuel n'est pas encore chargé, il est donc impossible de le sélectionner.

Une fois le rendu terminé et le useEffect déclaré, celui-ci va s'exécuter car c'est un useEffect correspondant au componentDidMount. La fonction va s'exécuter et récupérer #item-1. Le DOM existe bien, nous avons donc un résultat.

useEffect accepte 2 paramètres :

  • La fonction de callback (le process) qu'il doit exécuter
  • Un tableau d'éléments sur lequel il doit réagir

Pour savoir que c'est un useEffect qui intervient uniquement au componentDidMount, il vous faut regarder le 2eme paramètre. Dans notre cas, nous avons un tableau vide []. Cela veut dire que notre useEffect n'a aucun élément sur lequel réagir.

Il faut alors savoir que dans tous les cas, les useEffect doivent au moins réagir une fois : à l'évènement du componentDidMount.


Update - La mise à jour

Le cycle de vie de la mise à jour s'active dans 2 cas :

  • La modification du state, votre état local
  • La modification d'une props de l'un de vos parents

Dès qu'il intervient un de ces 2 cas, votre composant va déclencher un re-render. Dès lors, il va engager les différentes méthodes que l'on a vu au dessus, puis re-rendre le JSX avec la mise à jour de son état.

Nous allons prendre l'exxemple d'un compteur dans notre cas. Chaque seconde, nous allons incrémenter sa valeur.

Exemple avec des class components

Comme vous pouvez le constater, les 2 composants exécutent toutes les secondes un render().

On ne parle d'ici que de 2 composants mais imaginez une application complète... ! C'est là un point crucial lorsque vous allez développer avec React : savoir gérer correctement vos composants et le besoin de se re-rendre. C'est ce qui fait toute la différence entre une application performante et une autre. Si vous ne faites pas attention, vous pouvez provoquer du "bruit" dans votre application et déclencher des comportements inutiles.

Voyons voir de plus prés les 3 méthodes qui sont exécutées notamment au sein du composant Counter :

L'ordre est bien respecté

J'ai fais un console.log sur les différents paramètres du shouldComponentUpdate pour que l'on puisse constater les évolutions. Dans notre cas, seul le nextProps nous intéresse et l'on peut voir la valeur de la nouvelle propriétés.

Pour comprendre l'intérêt de shouldComponentUpdate, c'est une méthode qui va vous permettre d'intervenir sur le re-render du composant. J'ai mis un return true exprés car cette fonction vous demande de retourner un booléen. Cela va définir si oui ou non le composant a le droit d'effectuer la suite du cycle de vie de mise à jour et donc de générer un re-render.

Attention, jouer avec cette méthode demande de bien connaître et maîtriser ce que vous souhaitez faire. Ne cherchez pas à l'optimiser en permanence.

Tentons de mettre un return false pour comprendre son effet :

La propriété est bien a jour mais visuellement, le compteur ne l'est pas

En faisant un return false, vous bloquez le re-render et donc le visuel du composant. La question que vous devez donc vous poser est de savoir si votre composant a t'il visuellement besoin d'être mise à jour ? Par exemple, si vous avez un composant d'UI qui n'affiche que du texte, est-ce que celui-ci a t'il besoin d'être re-render si son parent l'est ? Pas forcément! Dans ces cas là, il est intéressant de bloquer et de prévoir le cas.

En ayant accès au prochain state et aux prochaines props, vous pouvez donc faire un comparatif entre l'état actuel et l'état qui arrive. Par exemple, je peux décider de re-render le compteur uniquement lorsque l'addition entre la nouvelle propriété et l'ancienne propriété est un multiple de 3 (on est d'accord, on ne le fera jamais... mais c'est un exemple).

0 - 1 - 3 - 4 - 6 - 7 - 9 - ...

Enfin, en ce qui concerne componentDidUpdate, il intervient au même moment que componentDidMount à la différence que le DidUpdate va s'exécuter à chaque re-render. Il ne s'exécute pas si votre shouldComponentUpdate retourne faux.

Exemple avec un functional component

Concernant les props d'un functional component, vous n'avez pas accès au shouldComponentUpdate, ni au componentDidUpdate. Tout comme le componentDidMount, Nous allons devoir nous retourner de nouveau vers le hook React useEffect

Tout comme le useEffect équivalent au componentDidMount, l'exécution des console.log ne change pas. Nous avons d'abord celui "Avant la déclaration du hook" puis, le "render" du composant et enfin le useEffect qui s'exécute.

Comme expliqué plus haut, un useEffect s'exécute forcément au moins une fois lors du montage du composant. Seulement, nous avons maintenant une différence avec notre ancien useEffect, nous avons un paramètre a "écouter" dans notre 2eme paramètre : [count]

Pour ce deuxième paramètre, vous ne pouvez traiter que des variables qui concernent l'état de votre composant donc : soit des props, soit un state. Ici, je souhaite donc "réagir" lorsque ma propriété count est modifiée. Nous sommes sur l'équivalent du componentDidUpdate.

Qu'en est-il du shouldComponentUpdate ?

Le shouldComponentUpdate n'existe pas pour un functional component.

Il vous utiliser la notion de "memoization" (ou mémorisation), plus communément appelé : memoize. Le principe de memoization est une technique qui permet d'exécuter une fonction (pure) une fois, puis enregistre le résultat en mémoire. Si nous essayons d'exécuter à nouveau cette fonction avec les mêmes arguments qu'auparavant, elle renvoie simplement le résultat que nous avons déjà obtenu qui est enregistré.

Pour faire cela, React nous met un disposition un composant d'ordre supérieur (HOC) appelé memo : React.memo()

En l'utilisant sur votre composant, le rendu de celui-ci sera enregistré. De fait, si aucun changement n'intervient sur le composant, il ne sera pas re-rendu.

Vous avez à votre disposition un 2eme paramètre qui correspond à un callback qui va venir évaluer la différence entre les propriétés. Dans shouldComponentUpdate nous avions nextProps et nextState. Ici, vous aurez les précédents :

Si vous voulez donc bloquer totalement le rendu de votre composant comme dans notre précédent exemple, il faut suffit de retourner false :

Voici une autre forme d'écriture du composant et de la memoization avec une arrow function :


Unmount - Le démontage

Le démontage d'un composant intervient lorsque celui-ci disparaît du DOM. Pour vous présenter cet exemple, je vais modifier un peu ce que l'on utilise depuis le début. C'est le Counter dorénavant qui va avoir dans son état local où en est le compteur (avant nous utilisions une propriété).

Je vais également un état qui va nous permettre de faire disparaître le composant au bout d'un certain temps.

Nous voyons le log du componentWillUnmount

Nous avons un beau Warning!! qui nous prévient que nous avons des "fuites de mémoire" dans notre application. Et c'est bien vrai... ! Le composant Counter n'est plus là, mais nous continuons d'avoir l'exécution du Tick!!!. Cette erreur est très dangereuse pour vos applications React car cela impacte directement la performance de votre application et vous pouvez tomber dans des boucles infinies qui ne vont pas faire plaisir à votre navigateur...

Dans notre cas, cette erreur est facilement visible car nous sommes sur un cas très petit. Imaginez une application complète, ce n'est pas la même chose.

Tout comme n'importe quel évènement JavaScript (un addEventListener, un setInterval, etc... ), ils sont enregistrés et ne s'arrêterons jamais si vous ne leur demandez pas explicitement de le faire. Qu'importe qu'un élément du DOM existe ou n'existe plus. Le seul moyen de l'arrêter si vous ne le faites pas est de fermer votre navigateur mais je n'appelle pas ça résoudre un problème.

Pour corriger cette erreur, il faut nous servir de l'évènement componentWillUnmount. Que doit-on faire une fois que le composant Counter n'existe plus : "clear" l'interval !

En ajoutant cette instruction, le problème est résolu !

Exemple avec un functional component

Comme d'habitude, notre ami le functional component ne peut pas accéder à cette fonction directement. Nous devons alors repasser par la case du useEffect. Voici l'équivalent exact du composant Counter en functional component avec le bug du componentWillUnmount

On peut s'apercevoir que le setCount est plus complexe que ce que l'on a déjà pu voir. Nous verrons cela plus en détail lors du sujet des hooks.

Pour reproduire cet équivalent, nous avons à notre disposition notre état local grâce au useState. Puis, je me place sur l'équivalent du componentDidMount avec le useEffect avec un tableau vide en 2eme paramètre : [].

Si vous avez bien compris l'enjeu du useEffect jusqu'à maintenant, vous comprendrez que le moyen d'intervenir sur le démontage réside toujours dans cette fonction. Chaque useEffect que vous déclarez vous donne accès à un callback permettant d'intervenir sur le démontage du composant. Il s'agit du retour de votre fonction dans le useEffect :

Nous n'avons plus qu'à l'utiliser pour "clear" notre interval et résoudre le problème de mémoire :


Ce cours est complexe et c'est l'un des points le plus important de la librairie React. Je vous invite a vous entrainer sur l'utilisation du cycle de vie et vous diriger vers la compréhension des hooks.

A partir de maintenant, je n'ai plus d'intérêt à travailler avec des class components puisque nous venons de voir les différences fondamentales entre les 2 types de composants. Tous les prochains cours utiliserons des functional components.