Appearance
Cycle de vie
Le moteur de réactivité permet, comme nous l’avons vu dans les chapitres précédents, d’avoir un recalcul automatique de certaines parties de nos composants en fonction de l’état des données réactives.
Il y a toutefois des cas où l’on souhaite que ces recalculs se fassent et d’autres fois non, car cela n’est pas pertinent ou peut nuire aux performances de l’application. Pour jouer plus finement avec la réactivité de notre application, nous allons aborder le concept de cycle de vie des composants, qui définit comment le framework réagit aux changements d’état au sain de l'application.
Etapes du cycle de vie
Avec la réactivité, la structure de notre application est amenée à changer au fil du temps. Une interaction utilisateur peut entraîner la création ou la destruction d’un composant, via une condition HTML par exemple. Ou, plus simplement, un changement d’état peut nécessiter la modification des éléments affichés dans un composant.
On identifie donc trois segments intervenant dans la vie d’un composant :
- Sa création (montage) : elle peut avoir lieu lors de l’initialisation de l’application, bien sûr, ou en réaction à un événement.
- Sa mise à jour : elle intervient chaque fois qu’une donnée réactive du composant change de valeur. Cela consiste à répercuter ce changement sur l’ensemble du composant. Ce segment a souvent lieu très régulièrement dans la vie d’un composant donné.
- Sa destruction (démontage) : elle a lieu lorsque le composant est retiré de l’application à la suite d’un événement particulier.
Chaque composant de l’application dispose de son propre cycle de vie.
On parle également de montage et de démontage pour désigner une partie de la création et de la destruction d’un composant, car ces segments consistent notamment, pour le framework, à traduire le composant en éléments HTML natifs et à ajouter ou retirer ces éléments du DOM réel de l’application.
Création
À la création d’un composant, le code JavaScript défini dans la déclaration du composant est exécuté pour initialiser les données réactives, les props et les fonctions internes du composant. Ce code est exécuté avant le montage du composant, c’est-à-dire avant qu’il soit ajouté au DOM réel de l’application.
Autrement dit, à ce stade, le composant n’existe que dans le DOM virtuel. C’est pourquoi il n’est pas possible d'y faire référence à des éléments du DOM réel comme on le ferait en JavaScript natif:
jsx
export default function App() {
// This don't work properly !
const divElement = document.getElementById("my-div");
return <div id="my-div">My div</div>;
}Pour effectuer une action après le montage du composant dans le DOM réel, il est possible d’utiliser un hook, une fonction à laquelle on spécifie en argument l’action à réaliser après le montage du composant :
jsx
import { useEffect } from "react";
export default function App() {
useEffect(() => {
const divElement = document.getElementById("my-div");
divElement.textContent = "New div content";
console.log(divElement.textContent);
}, []);
return <div id="my-div">My div</div>;
}Nous retrouvons ici la fonction useEffect de React.js qui prend en premier argument la fonction à exécuter une fois le composant monté. useEffect peut en effet être utilisé pour effectuer une action après le montage du composant. Dans ce cas, on laisse généralement la fonction sans dépendances, en second argument, ce qui fait qu'elle n'est jamais relancée une deuxième fois.
Dans cet exemple, le hook nous permet de récupérer une référence vers la balise <div> via son id, comme en JavaScript natif. Nous modifions ensuite le contenu de la balise et affichons ce contenu dans la console.
NOTE
👉 On privilégiera généralement toujours de passer par le DOM virtuel pour interagir avec les balises présentes dans le composant, en utilisant les techniques vues dans les chapitres précédents. Les références vers le DOM réel peuvent toutefois être nécessaires dans des cas plus particuliers, notamment lors de l’utilisation d’une librairie tierce en Javascript natif qui n’utilise pas de composants (data-visualisation, canvas 3D, etc.).
INFO
📖 De façon plus générale en programmation, un hook est un mécanisme permettant de modifier le comportement d’une système sans avoir à modifier son code source. Dans notre cas, le système est le cycle de vie mis en place par le framework. Le hook permet de personnaliser son fonctionnement sans avoir à toucher au code source du framework.
Mise à jour
La mise à jour du composant est le segment le plus fréquent dans la vie d’un composant. Elle est activée à chaque fois que la valeur d’une donnée réactive ou d'une props définie par le composant est modifiée.
La façon dont la mise à jour est traitée diffère en fonction du framework :
Recalcul du composant
En React.js, lorsqu’une mise à jour a lieu, l’ensemble du composant est par défaut re-rendu. Cela signifie que la fonction définissant le composant est exécutée à nouveau. Seules les valeurs des données réactives ne sont pas réinitialisées et conservent leur état précédent la mise à jour (sauf pour la donnée faisant l’objet de la mise à jour, bien sûr).
Reprenons l’exemple d’un compteur, comme dans un chapitre précédent :
jsx
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
let count2 = 0;
const increment = () => {
setCount((oldCount) => {
const newCount = oldCount + 1;
console.log(newCount);
return newCount;
});
count2 += 1;
console.log(count2);
};
return (
<div>
<p>{count}</p>
<p>{count2}</p>
<button onClick={increment}>Add</button>
</div>
);
}Si vous exécutez cet exemple, vous constaterez que la variable count2 vaut toujours 0 après incrément, contrairement à count, qui s’incrémente à chaque clic comme attendu. Cela s’explique par le fait que React.js redéfinit l’ensemble du composant à chaque changement d’état, comme ici lorsque la valeur de count change. Lors de cette mise à jour, la variable count2 est donc d’abord détruite pour être réinstanciée avec sa valeur initiale, ici 0.
INFO
📖 On dit que le framework fonctionne de manière déclarative. C’est-à-dire que l’état actuel de l’application peut être déterminé uniquement à partir des données réactives des composants et leur déclaration.
Modification multiples
Dans l’exemple précédent, vous pouvez également remarquer que la valeur de count2 est affichée dans la console avant celle de count. C’est parce que React.js attend la fin de l’exécution des événements en cours avant de mettre à jour le composant. Cela implique que si un même événement entraîne la modification de plusieurs données réactives en même temps, la mise à jour du DOM ne s’effectuera qu’une seule fois, pour l’ensemble des modifications apportées
jsx
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [sum, setSum] = useState(0);
const increment = () => {
setCount((oldCount) => oldCount + 1);
setSum((oldSum) => oldSum + count + 1);
};
return (
<div>
<p>{count}</p>
<p>{sum}</p>
<button onClick={increment}>Add</button>
</div>
);
}Dans cet exemple, la modification des valeurs count et sum a lieu à chaque incrément. L’incrément n’entraîne pourtant qu’une seule fois le recalcul du composant. On dit que React.js groupe (batch) les mises à jour d’état.
WARNING
⚠️ Attention à ne pas s’attendre à ce que la donnée count soit incrémentée dès la déclaration setCount(oldCount => oldCount + 1). L'increment s'effectue après la fin de l'évènement, lorsque le composant est re-rendu. Par conséquent, dans la déclaration setSum(oldSum => oldSum + count + 1), nous devons considérer que l'incrément n'a pas encore eu lieu quand nous nous référons à count.
INFO
📖 On dit que la mise à jour du composant s'effectue de manière asynchrone.
Mise à jours des composants enfants
Si le composant mis à jour comporte des composants enfants, ceux-ci seront également recalculés. Même si le composant enfant n'exploite pas la donnée qui a entrainer la mise à jours il sera re-rendu :
jsx
import { useState } from 'react';
import MyChild from './components/MyChild.jsx';
import MyOtherChild from './components/MyOtherChild.jsx';
export default function App() {
const [count, setCount] = useState(0);
const increment = () => setCount(oldCount => oldCount + 1)
return (
<div>
{/* recomputed if increment */}
<MyChild count={count}/>
{/* also recomputed if increment */}
<MyOtherChild />
<button onClick={increment}>Add</button>
</div>
);
}Si l'on ne souhaite pas qu'un composant enfant soit constamment re-rendu lorsque son parent est mis à jour, pour des raisons de performances par exemple, il est possible de déclarer le composant à l'aide de la fonction memo :
jsx
import { memo } from 'react';
const MyChild = memo(({count}) => {
console.log('MyChild rendered')
return <div>{count}</div>;
})De manière similaire à useMemo, mais appliqué à l'ensemble du composant, memo effectue une mémoïsation sur celui-ci. Le composant ne sera alors re-rendu à cause de son parent que si une mise à jours de celui-ci entraîne un changement de valeur dans les props du composant.
useEffect : maitriser certains recalculs
La mise à jour d’un état entraîne donc le recalcul de l’ensemble du composant. Mais il arrive toutefois que l’on ne souhaite pas que certaines opérations soient recalculées à chaque mise à jour. C’est le cas notamment lorsque l’on effectue des opérations asynchrones, comme une requête à un serveur, ou lorsqu’on effectue une action à intervalle de temps régulier.
Tentons maintenant de rendre notre compteur automatique, en utilisant la fonction Javascript native setInterval pour incrémenter count à chaque seconde :
jsx
import { useState, useEffect } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
}, []);
return (
<div>
<p>{count}</p>
</div>
);
};Dans cet exemple, nous avons utilisé la fonction setInterval afin de lancer l’incrément du compteur à chaque seconde (1000 millisecondes).
La fonction setInterval est appelée au sein d’un useEffect. useEffect nous permet de contrôler quand la fonction doit être recalculée ou non. En l'occurrence ici, nous souhaitons qu'elle ne soit calculée qu'à la création du composant. Aucune donnée n'est donc listée dans les dépendances, en second argument de la fonction.
Si la fonction setInterval n’était pas appelée via useEffect, mais directement dans la déclaration du composant, la fonction serait appelée à nouveau chaque fois que l’état count est modifié. Cela aurait pour conséquence de multiplier l’application de l’incrément à chaque seconde qui passe.
INFO
👉 Si l'environnement de développement est en StrictMode, il se peut que l'incrément soit exécuté deux fois. Il s'agit d'une protection, qui n'a pas lieu en production, et qui nous incite à utiliser une fonction de nettoyage (voir plus bas).
useEffect : ajouter des dépendences
Imaginons que l’on souhaite maintenant pouvoir doubler ou diviser la fréquence à laquelle l’incrément s’effectue à l’aide de deux boutons :
jsx
import { useState, useEffect } from "react";
export default function App() {
const [incrementInterval, setIncrementInterval] = useState(1000);
const [count, setCount] = useState(0);
const double = () => setIncrementInterval((interval) => interval * 2);
const divideByTwo = () => setIncrementInterval(
(interval) => interval / 2
);
useEffect(() => {
console.log("Auto-increment with interval: ", incrementInterval);
setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, incrementInterval);
}, [incrementInterval]);
return (
<div>
<p>{count}</p>
<button onClick={double}>Double</button>
<button onClick={divideByTwo}>Divide by two</button>
</div>
);
};Nous ajoutons un nouvel état incrementInterval que nous utilisons dans l’appel de setInterval pour définir la fréquence à laquelle l’incrément se réalise. Puis, nous ajoutons deux boutons qui permettent de modifier la valeur de l’intervalle. Enfin, incrementInterval est ajouté à la liste des dépendances du useEffect, en second argument de la fonction.
Cela indique à React.js qu’une modification de la valeur incrementInterval doit entraîner un recalcul de la fonction associée au useEffect.
Si l’on teste l’exemple précédent, on constate bien que la modification de incrementInterval entraîne un nouvel appel de setInterval, qui prend alors en compte le changement d’intervalle. Toutefois, l’incrément initial continue de s’exécuter : chaque recalcul ajoute un nouvel incrément qui se cumule avec le précédent. Pour résoudre ce problème, il faut définir une fonction permettant de nettoyer (clean) les effets du useEffect provenant de son calcul précédent.
useEffect : fonction de nettoyage
Les fonctions définies via useEffect peuvent avoir un effet persistant sur l’application, que l’on peut souhaiter supprimer si la fonction associée est recalculée. C’est le cas avec l’usage de setInterval. Pour mettre fin aux effets d’un setInterval, la fonction retourne un objet lorsqu’elle est appelée. Cet objet peut être passée en argument à la fonction native clearInterval, qui se chargera de mettre fin à l’opération :
js
const timer = setInterval(() => {
console.log("Running");
}, 1000);
...
clearInterval(timer); // Stop intervalDans notre cas, nous aimerions stopper l’intervalle au moment où le useEffect est recalculé, et qu’un nouveau setInterval est alors défini. Nous pourrions tenter de stocker globalement l’objet retourné par l’intervalle précédent et l’utiliser dans le useEffect, mais il y a une solution plus simple. Il est possible de définir une fonction de nettoyage en valeur de retour dans la fonction fournie au useEffect :
jsx
import { useState, useEffect } from "react";
export default function App() {
const [incrementInterval, setIncrementInterval] = useState(1000);
const [count, setCount] = useState(0);
const double = () => setIncrementInterval((interval) => interval * 2);
const divideByTwo = () => setIncrementInterval(
(interval) => interval / 2
);
useEffect(() => {
const counter = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, incrementInterval);
return () => {
clearInterval(counter);
};
}, [incrementInterval]);
return (
<div>
<p>{count}</p>
<button onClick={double}>Double</button>
<button onClick={divideByTwo}>Divide by two</button>
</div>
);
};La fonction ainsi retournée sera appelée par React.js lors du prochain recalcul du useEffect. On appelle cette fonction la fonction de nettoyage (clean) du useEffect.
En résumé, chaque modification de l’état incrementInterval entraîne une mise à jour du composant. L’état étant listé parmi les dépendances du useEffect, sa modification entraîne un recalcul de la fonction associée au useEffect. Avant ce recalcul, la fonction de nettoyage est appelée afin de supprimer l’intervalle précédemment défini. L’intervalle est ensuite redéfini en prenant en compte le changement d’état.
Destruction
Un composant peut être détruit si un changement d’état au niveau de son composant parent entraîne son retrait de la structure de l’application. Il est relativement rare de vouloir effectuer une action au moment de la suppression d’un composant. Il est toutefois possible de le faire avec l’appel d’un hook :
jsx
import { useEffect } from "react";
export default function App() {
useEffect(() => {
return () => {
console.log("Component destoyed");
};
}, []);
return <div>My Component</div>;
};Les fonctions de nettoyage des useEffect sont également appelées à la destruction du composant. C’est pourquoi il est possible d’utiliser useEffect et d’y associer uniquement une fonction de nettoyage. En laissant la liste des dépendances vide, seule la destruction du composant entraînera l’appel de la fonction de nettoyage.
INFO
👉 Si vous êtes en StrictMode, le composant sera rendu deux fois. Vous aurez donc l'exécution d'une première destruction, uniquement en mode développement.
🐶 Dog Center
Avec une meilleure comprehension de la réactivité des composants, nous pouvons maintenant faire en sorte de récupérer les données de nos chiens, non plus depuis un fichier, mais via une requête à un serveur distant.
Pour cela, définissons d'abord une fonction chargée d'effectuer la requête au serveur à l'aide de la fonction fetch :
js
const getDogsData = async () => {
const response = await fetch("https://dog-center.com/api/dogs");
if (response.status === 200) {
const newData = await response.json();
return newData;
}
};
export { getDogsData };Comme nous effectuons une requête à un serveur, il s'agit d'une fonction asynchrone.
TIP
📖 Si vous n'êtes pas familier avec les fonctions asynchrones et les requêtes d'API, vous trouverez davantage d'informations dans la notion de cours sur fetch et l'asynchrone
La fonction est définit dans le fichier src/services/api.js.
Poursuivons en remplaçant l'import des données depuis un fichier par l'appel de notre fonction getDogsData au niveau de notre composant principal :
jsx
import "./App.css";
import { useState, useMemo, useEffect } from "react";
import { getDogsData } from "./services/api";
import NewsSlider from "./components/NewsSlider";
import DogCard from "./components/DogCard";
export default function App() {
const [dogsData, setDogData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const data = await getDogsData();
setDogData(data);
}
fetchData();
}, [])
...
return (
<div>
...
</div>;
)
};Nous utilisons ici useEffect afin de nous assurer que la requête au serveur ne s'effectue qu'à l'initialisation du composant, et non lors de sa mise à jour. Les données récupérées sont placées dans une nouvelle donnée réactive nommée dogsData remplaçant les données du fichier.
NOTE
👉 La fonction passée en argument à useEffect ne peut pas être asynchrone. Par conséquent, nous créons ici une fonction interne que nous appelons immédiatement après.
WARNING
⚠️ Attention à bien initialiser la valeur de dogsData avec un tableau vide. Comme la requête au serveur est asynchrone, le composant est créé avant la réception des données. Il utilisera pendant quelques millisecondes la valeur par défaut de dogsData pour construire la galerie. Le composant cherchera alors à parcourir dogsData afin de générer des DogCard. Il faut donc veiller à ce que dogsData contienne toujours une valeur pouvant être parcourue comme un tableau, sinon le composant tombera en erreur.
Conclusion
Le cycle de vie des composants permet de mieux appréhender la façon dont les composants évoluent en fonction des changements d'état.
C'est une notion plus abstraite et complexe, mais indispensable pour maîtriser plus pleinement le comportement de l'application lorsque l'utilisateur interagit avec elle, et pour être capable de déboguer correctement des scénarios d'interactions plus élaborés.
Ce chapitre clôture cette partie sur la réactivité. Dans la prochaine partie, nous explorerons des concepts plus avancés s'appuyant à la fois sur les composants et d'interactivité.

