Je pensais que les Hooks de React étaient entrées dans les mœurs et que tout le monde était devenu expert. Cependant, après quelques discutions par-ci, par-là, j’ai découvert que ce n’était pas le cas. J’espère que cette suite d’article pourra les aiguiller vers la maitrise de cette “hype” du passé.
On commence comme toujours par la base. Pour moi, les Hooks les plus souvent utilisés sont les suivants :
useState
useEffect
useRef
On va découvrir et redécouvrir les Hooks ensemble. J’espère donc que vous possédez une connaissance minimale en JS et React.
Récap
Les Hooks sont une nouvelle forme de gestion du cycle de vie de React. Ils sont arrivés avec la version 16.8.0. Précédemment, les composants React pouvaient être écrit de 2 manières.
En utilisant une classe :
class Welcome extends React.Component {
render() {
return (
<div>
<p>Welcome</p>
</div>
);
}
}
En utilisant une fonction :
function Welcome() {
return (
<div>
<p>Welcome</p>
</div>
);
}
L’inconvénient des composants fonctionnelles était l’absence de du cycle de vie de React. Impossible d’utiliser du state
ou d’interagir avec le composant en fonction de son état de rendu (componentDidMount
, …).
Les Hooks arrivent donc ici. C’est ce qui va permettre de donner vie aux composants fonctionnels. Mais attention, React n’a pas mis ça en place uniquement afin de reproduire les effets du cycle de vie des classes. Ils ont décidé de changer le point de vue du développement pour avoir une approche, comme ces composants, plus fonctionnelle.
La base
L’utilisation des Hooks doit respecter deux règles fondamentales :
- Les Hooks ne peuvent être appelés que dans un composant fonctionnel de React ou un autre Hook.
- Les Hooks doivent toujours être appelés dans le même ordre. Dans votre composant, si les Hooks sont exécutés dans des conditions, boucles ou sous fonctions, l’ordre d’appel doit toujours être le même. Il est fortement recommandé de garder l’appel des Hooks au premier niveau du composant.
À partir de là vous pouvez appeler plusieurs fois le même dans un composant. Libre à vous de les utiliser dans l’ordre que vous souhaitez.
useState
C’est avec cette fonction qu’on va gérer l’état (comme son nom l’indique) du composant.
const [state, setState] = useState(initialState);
Voilà la syntaxe proposée par React. Qu’avons-nous là ?
[state, setState]
: Le tableau retourné par la fonctionuseState
contient la valeur de l’état (state
) et la fonction permettant de l’éditer (setState
).state
: la valeur de l’état que l’on va pouvoir utiliser partout dans le composantsetState
: la fonction qui va permettre de mettre à jour l’état (setState(newValue)
)initialState
: la valeur initiale de l’état. Si aucune valeur n’est fournie, l’état est initialisé ànull
.
Et vous pouvez en créer plusieurs dans le même composant.
function Exemple() {
const [total, setTotal] = useState(0);
const [value, setValue] = useState("defaultValue");
//[...]
}
C’est d’ailleurs la force de cette notation. On va préférer segmenter les états plutôt que de d’en avoir qu’un seul contenant plusieurs valeurs. Cela devient plus facile de séparer la logique et de mettre à jour l’état du composant.
Un exemple !
On va prendre le bon vieil exemple du compteur.
// Importation du useState
import React, { useState } from "react";
function Exemple() {
// On initialise le state ici, avec 0 comme valeur par défaut
const [count, setCount] = useState(0);
return (
<div>
{/* On utilise la valeur du state ici pour l'afficher */}
<p>Eeeeeeeeeeeet le compteur est actuellement égal à : {count}</p>
{/* On utilise la fonction de modification pour ajouter 1 à la valeur actuelle */}
<button onClick={() => setCount(count + 1)}>
C'est ici qu'il faut cliquer
</button>
</div>
);
}
L’exo
QUOI! Un exercice?
Eh oui, je ne vais pas être le seul à bosser ici !
Je vous ai créé un petit CodePen pour mettre en place votre premier (…ou pas) useState
.
Dans celui-ci, vous retrouverez un compteur. Cependant, le bouton pour réduire le compte ne fonctionne pas. À vous de mettre en place cette fabuleuse nouvelle fonctionnalité. Et on aimerait que la dernière action soit affichée dessous. Histoire de savoir lequel des boutons a été cliqué en dernier.
Je vais quand même vous donner un indice : il faut utiliser useState
. ;)
L’Exercice –> useState - Exercice
Note : Les imports dans un projet JS sont souvent sous la forme import { useState } from 'react'
. Étant donné l’environnement de CodePen, les imports dans certains exemples seront sous la forme const { useState } = React
.
Bonne chance !
À noter
setState
La fonction de modification d’état peut aussi prendre une fonction en paramètre. Dans ce cas, le paramètre de la fonction sera l’ancienne valeur de l’état.
const [count, setCount] = useState();
//[...]
// Cette notation est équivalente à setCount(count + 1)
setCount((oldCount) => oldCount + 1);
initialState
Le paramètre utilisé par useState
(initialState
) peut aussi être une fonction.
const [count, setCount] = useState(() => {
const initialCount = internalCountMethodThatMayBeExpensiveSomeHow(props);
return initialCount;
});
Cette fonction ne sera exécutée qu’au premier rendu.
Rendu
Si la nouvelle valeur de l’état est la même que la valeur actuelle, aucun rendu ne sera fait et donc aucun effet ne sera exécuté.
Attention: La comparaison effectué par React est un Object.is
.
Solution
Voilà une des solutions possibles pour l’exercice plus haut :
useEffect
Maintenant qu’on sait comment gérer l’état de notre composant, on va s’intéresser aux “effets de bord”. Ce sont les actions qui peuvent affecter d’autres composants et qui ne peuvent pas être réalisées pendant l’affichage. On va retrouver le chargement de données depuis une source externe, les abonnements à des évènements du navigateur ou encore la réaction à l’évolution d’autres composants. Et pour faire tout ça, on va utiliser useEffect
.
On aura donc cette syntaxe.
useEffect(effectFunction, conditionalArray);
On a donc:
effectFunction
: fonction d’effet exécutéeconditionalArray
: optionnel, tableau de propriétés conditionnant l’exécution de l’effet
La fonction d’effet s’exécute, par défaut, à chaque rendu. A chaque fois, qu’un des state
ou qu’une des props
est modifié.
Si on reprend l’exemple précédent, on peut ajouter un effet à chaque modification du compteur.
// Importation du useEffect
import React, { useState, useEffect } from "react";
function Exemple() {
const [count, setCount] = useState(0);
// initialisation de l'effet
useEffect(() => {
document.title = `Compteur : ${count}`;
});
return (
<div>
<p>Eeeeeeeeeeeet le compteur est actuellement égal à : {count}</p>
<button onClick={() => setCount(count + 1)}>
C'est ici qu'il faut cliquer
</button>
</div>
);
}
L’effet va s’exécuter au premier rendu et affichera donc dans le titre Compteur : 0
.
Puis à chaque fois que le composant fera un rendu. Dans notre cas, à chaque fois que le compteur sera modifié.
Exécution conditionnelle
La condition va permettre d’effectuer un effet en fonction d’une propriété ou de l’état du composant. Notre effet n’a pas besoin d’être mis à jour à chaque modification du composant, mais uniquement si le compte change. On va donc se retrouver avec la syntaxe suivante :
useEffect(() => {
document.title = `Compteur : ${count}`;
}, [count]);
Dans l’exemple précédent, si notre composant Exemple
recevait des propriétés, l’effet se serait exécuté.
Une fois la condition ajoutée, l’effet ne s’active que lorsque la valeur count
est modifiée.
L’effet peut donc aussi s’exécuter à la suite d’une modification de propriétés.
function Exemple(props) {
const [count, setCount] = useState(0);
useEffect(() => {
// Si la valeur `value` reçu des propriétés change, le compteur se mettra à jour
setCount(props.value);
// On conditionne l'exécution de cet effet sur l'évolution de la propriété `value`
}, [props.value]);
return (
<div>
<p>{count}</p>
{/* Mais la valeur du compteur peut toujours être modifiée avec le bouton */}
<button onClick={() => setCount(count + 1)}> Plus 1 </button>
</div>
);
}
Nettoyage
Il est possible d’exécuter un effet de ‘nettoyage’ en retournant une fonction dans notre useEffect
.
useEffect(() => {
document.title = `Compteur : ${count}`;
return () => {
document.title = "";
};
}, [count]);
Les effets se dérouleront dans l’ordre suivant :
- Le composant s’affiche pour la première fois
document.title
passe à'Compteur : 0'
- Clic sur le compteur
document.title
passe à''
document.title
passe à'Compteur : 1'
- Le composant est supprimé (pour une quelconque raison)
document.title
passe à''
C’est surtout utile pour les souscriptions à des évènements :
useEffect(() => {
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mouseup", handleMouseUp);
};
});
L’exo
Mettons tout cela en pratique. On reprend le composant GreatCounter
.
La prochaine fonctionnalité sera d’émettre une alerte lorsque le compteur est égal à 10 ou à -10.
Je vous laisse le soin de trouver les “wordings” pour ces alertes. ;)
Exercice : useEffect - Exercice
À noter
Plusieurs effets
Tout comme pour useState
, vous pouvez utiliser plusieurs useEffect
dans le même composant.
useEffect(() => {
if (cartItems.length) {
console.log("Il y a des objets dans le panier !");
}
}, [cartItems]);
useEffect(() => {
document.title = loading ? "Chargement en cours" : "La page est chargée";
}, [loading]);
Un seul effet
Comme vu plus haut, sans tableau de condition, l’effet s’exécute à chaque rendu. Avec un tableau de propriétés ou d’état, l’effet s’exécute lors de la modification de ceux-ci.
Du coup, pour que l’effet ne s’exécute qu’une seul fois, vous pouvez passer un tableau vide.
async function fetchData() {
const res = await fetch("https://yourapi.org/");
res.json().then(doWithResult).catch(manageError);
}
useEffect(() => {
fetchData();
}, []);
Asynchrone
La notation ci-dessus n’est pas anodine. La fonction passée en premier paramètre à useEffect
doit retourner une fonction ou rien. Ceci n’inclut pas les Promise
. On ne peut donc pas utiliser async/await
sur cette fonction.
La syntaxe suivante est donc erronée :
useEffect() => {
// Permet de contrôler l'état du 'fetch'
const controller = new AbortController();
// On exécute la fonction asynchrone à l'intérieur du useEffect
(async () => {
try {
const res = await fetch("https://yourapi.org/", { signal: controller.signal });
const json = await res.json();
// Utiliser les informations reçu (par exemple, modifier l'état du composant)
await doWithResult(json);
} catch (err) {
manageError(err);
}
})();
return () => {
// Si le composant est supprimé avant la fin de la récupération de données, l'appel est annulé
controller.abort();
};
});
Solution
Voici une des solutions possibles de l’exercice :
useRef
useRef
va renvoyer un objet modifiable qui n’impactera pas le cycle de vie du composant. La syntaxe est la suivante.
const refContainer = useRef(initialValue);
refContainer
: Objet avec une propriétécurrent
initialValue
: Optionnel, c’est la valeur donnée à la propriétécurrent
du retour deuseRef
Référence de composant ou d’élément du DOM
L’usage le plus commun de cet Hook est la référence d’un composant.
import { useRef } from "react";
function InputWithFocus() {
// Définition de la référence
const inputRef = useRef();
// Lors du clic sur le bouton, la fonction 'focus' de l'input sera appelée
const onButtonClick = () => {
// On utilise la propriété 'current' de la référence pour y accéder
inputRef.current.focus();
};
return (
<>
{/* Initialisation de la référence */}
<input ref={inputRef} type="text" />
<button onClick={onButtonClick}>Focus</button>
</>
);
}
// ...
Variable mutable
Mais c’est surtout intéressant pour garder une variable modifiable qui peut être utilisée sans influencer le reste du comportement.
Ici, on va pouvoir utiliser l’id de l’intervalle pour pouvoir l’arrêter n’importe où dans le composant.
// ...
function Minuteur() {
// Définition de la référence
const intervalRef = useRef();
const [timer, setTimer] = useState(30);
useEffect(() => {
const id = setInterval(() => {
// On décrémente le minuteur géré par le state
setTimer((oldTimer) => oldTimer - 1);
}, 1000);
// Mise à jour de la référence
intervalRef.current = id;
return () => {
// Arrêt du ‘timer’ en cas de suppression ou réaffichage du composant
clearInterval(intervalRef.current);
};
}, []);
// Fonction permettant d'arrêter le ‘timer’
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
{/* Le timer est affiché ici */}
<p>Il reste : {timer} secondes</p>
{/* En cliquant sur ce bouton, le minuteur (et donc l'intervalle) sera arrêté */}
<button onClick={stopTimer}> STOP! </button>
</div>
);
}
// ...
Extérieur au cycle de vie
La modification de cette variable ne déclenchera pas de nouveau rendu.
Dans cette exemple, l’affichage du compteur restera à 0
. De même, si des effets étaient en place, aucun d’entre eux ne se déclencheraient.
import React, { useRef } from "react";
function Exemple() {
const counter = useRef(0);
const addCount = () => {
counter.current++;
};
return (
<div>
<p>Eeeeeeeeeeeet le compteur est actuellement égal à : {count}</p>
<button onClick={addCount}>C'est ici qu'il faut cliquer</button>
</div>
);
}
L’exo
Dans cet exercice, on oublie le compteur. On va faire un lecteur audio ! Le but va être d’implémenter les boutons play
et pause
du lecteur.
Il faudra utiliser les méthodes play
et pause
de l’élément audio
.
L’Exercice : useRef - Exercice
Solution
Voici une des solutions possibles :
Conclusion
J’apprécie particulièrement la facilité d’utilisation et la nouvelle vision que ça induit dans la construction des applications.
Pour continuer de s’améliorer sur ces points, entrainez-vous. Il n’y a que ça qui fonctionne. Pour ma part, ça a commencé par la réécriture de certains composants écrits avec des classes. Puis, la création de nouveaux composants avec des Hooks. Avec les petits, comme les boutons, au début, puis, les vues ou les éléments plus complexe.
Je vous souhaite bonne chance pour la suite !