useState
et useEffect
. Grâce à eux, nous avions appris à simplifier notre code pour s’affranchir de la complexité des classes. Aujourd’hui, nous sommes en compagnie de Noé, pour apprendre à créer nos propres hooks, afin de concocter des parties réutilisables de code.Pourquoi vouloir créer nos propres hooks ?
Dans un projet, l’un des enjeux principaux est toujours d’avoir du code générique et réutilisable. C’est après tout ce que nous propose React grâce au JSX : des que l’on peut imbriquer les uns dans les autres en leur donnant des props. Le hic, c’est que ces composants ne permettent de réutiliser que de la logique d’affichage, pas de la logique à état.
Considérons par exemple un code assez classique : un composant qui fait une requête HTTP lors de son chargement initial.
javascript const Composant = () => { const [data, setData] = useState([]); useEffect(() => { axios.get('http://monapi.fr/chose') .then(response => { setData(response.data); }); }, []); return (
); }
Il nous a fallu sept lignes de code rien que pour exposer notre requête, avant même d’écrire le JSX propre à notre composant, et ces sept lignes seront probablement répétées dans tous les composants faisant une requête… un vrai désastre.
Que faudrait-il faire ? Pouvoir extraire cette logique dans une fonction, afin de pouvoir écrire un code dans le style :
javascript const Composant = () => { const data = useAjax('http://monapi.fr/chose'); return (
); }
Vous l’aurez compris, useAjax sera un hook custom, c’est-à-dire une fonction qui appelle d’autre http://hooks.arcomme par exemple les fonctions d’état / d’effet qui permettent de composer et de rendre réutilisable une partie du code.
Quelques exemples de hooks customs
useToggle
Probablement l’exemple le plus simple pour toucher du doigt le concept. Considérons le code suivant :
Si l’utilisation de useState vous laisse perplexe, c’est peut-être le moment d’aller voir notre précédent article.
jsx const Composant = () => { const { isHappy, setIsHappy } = useState(true); return ( <>
Cliquez sur le bouton pour changer d’humeur
<> ); }
Lorsque l’on clique sur le bouton, on appelle son écouteur onClick, dans lequel on appelle la fonction de modification de notre état setIsHappy
avec l’inverse de la valeur courante (!isHappy
). Le bouton affichera ensuite deux textes différents selon la valeur contenue dans isHappy
, grâce à un ternaire.
On pourrait alors vouloir simplifier cette ligne : onClick={() => setIsHappy(!isHappy)}
en déclarant une fonction toggleIsHappy
. Ce qui donnerait :
jsx const Composant = () => { const [isHappy, setIsHappy] = useState(true); const toggleIsHappy = () => setIsHappy(!isHappy); return ( <>
Cliquez sur le bouton pour changer d’humeur
<> ); }
Encore mieux ! Notre onClick est beaucoup plus simple.
Mais imaginons que nous désirions faire la même chose, basculer un booléen, dans un autre composant. Nous devrions donc recopier tout ce code :
javascript const [isHappy, setIsHappy] = useState(true); const toggleIsHappy = () => setIsHappy(!isHappy);
Vous l’aurez une nouvelle fois compris, nous allons créer un hook custom.
Commencons par déclarer une fonction, puis déportons ce code dedans. Les hooks custom doivent toujours commencer par *use* :
javascript const useToggle = () => { const [isHappy, setIsHappy] = useState(true); const toggleIsHappy = () => setIsHappy(!isHappy); }
Ensuite, renommons les variables pour quelque chose de plus générique :
javascript const useToggle = () => { const [isActive, setIsActive] = useState(true); const toggle = () => setIsActive(!isActive); }
Ajoutons un paramètre, pour la valeur par défaut de notre état :
javascript const useToggle = (initialValue) => { const [isActive, setIsActive] = useState(initialValue); const toggle = () => setIsActive(!isActive); }
Et enfin, il nous faut renvoyer deux choses depuis notre hook :
– la donnée
– la fonction toggle.
Pour renvoyer plusieurs choses, on peut utiliser au choix un tableau ou un objet. Nous allons utiliser un tableau, pour rester similaire a useState :
javascript const useToggle = (initialValue) => { const [isActive, setIsActive] = useState(initialValue); const toggle = () => setIsActive(!isActive); return [isActive, toggle]; }
Notre code final serait alors :
jsx const useToggle = (initialValue) => { const [isActive, setIsActive] = useState(initialValue); const toggle = () => setIsActive(!isActive); return [isActive, toggle]; } const Composant = () => { const [isHappy, toggleIsHappy] = useToggle(true); return ( <>
Cliquez sur le bouton pour changer d’humeur
<> ); }
On vous l’accorde, ces quelques lignes peuvent être quelque peu déroutantes. Ce qu’il faut comprendre c’est que l’on appelle la fonction useToggle, qui renvoie un tableau. On déstructure ensuite ce tableau dans le composant : const [isHappy, toggleIsHappy] = useToggle(true);
dans deux variables isHappy
et toggleIsHappy
. Si c’est un peu abstrait, vous pouvez réviser la déstructuration par ici !
Voici notre petit hook custom ! Faible en termes de fonctionnalités, mais grand par l’ambition. 😁
Voyons maintenant quelques exemples un peu plus complexes.
useAjax
Reprenons l’exemple plus haut, de la récupération de données depuis une API.
javascript const Composant = () => { const [data, setData] = useState([]); useEffect(() => { axios.get('http://monapi.fr/chose') .then(response => { setData(response.data); }); }, []); return (
); }
Nous pourrions en faire un hook custom, de la même façon :
javascript const useAjax = (url) => { const [data, setData] = useState(); useEffect(() => { axios.get(url) .then(response => { setData(response.data); }); }, []); return [data]; } const Composant = () => { const [data] = useAjax('http://monapi.fr/chose'); return (
); }
Ici, notre hook custom déclare un state interne, il utilise le hook d’effet pour récupérer les données, puis sauvegarde les données.
Le hook renvoie les données de son state. Comme le hook est rappelé à chaque nouveau rendu depuis le code d’un composant de type fonction, il sera rappelé lors de la modification du state, dans le .then, ce qui permettra de tenir la donnée à jour.
On peut ensuite faire évoluer notre hook, par exemple pour gérer un état de chargement :
javascript const useAjax = (url) => { const [data, setData] = useState(); const [isLoading, setIsLoading] = useState(false); useEffect(() => { setIsLoading(true); axios.get(url) .then(response => { setIsLoading(false); setData(response.data); }); }, []); return [data, isLoading]; }
De la même façon, on pourrait gérer les autres paramètres HTTP : POST, DELETE, PATCH et PUT, en ajoutant un second paramètre a notre hook, ainsi qu’un troisième pour le body de la requête :
javascript const useAjax = (url, method = 'get', body) => { const [data, setData] = useState(); const [isLoading, setIsLoading] = useState(false); useEffect(() => { setIsLoading(true); axios({ method, url, data: body }) .then(response => { setIsLoading(false); setData(response.data); }); }, []); return [data, isLoading]; }
Nous voila donc avec un hook générique pour parler à une API dans tous nos composants !
useLocalStorage
Un dernier exemple intéressant, serait de créer un hook gérant un state qui serait persisté dans le local storage du navigateur.
jsx // Usage const Composant = () => { // Similaire a useState, mais le state est persisté dans le local storage sous la clé "name", et sa valeur initiale est "Bob" const [name, setName] = useLocalStorage("name", "Bob"); return (
); } // Hook function useLocalStorage(key, initialValue) { // le state qui contiendra notre valeur // Il est possible de donner une fonction d’initialisation a useState const [storedValue, setStoredValue] = useState(() => { try { // Essayer de trouver dans le localstorage selon la key const item = window.localStorage.getItem(key); // Parser le résultat si il existe, sinon renvoyer la valeur initiale return item ? JSON.parse(item) : initialValue; } catch (error) { // En cas d’erreur (par exemple local storage indisponible) renvoyer aussi la valeur initiale console.log(error); return initialValue; } }); // Une fonction de modification, qui modifiera le state, et le local storage correspondant const setValue = (value) => { try { // Modifier le state setStoredValue(valueToStore); // Modifier le localstorage window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { // Une version plus avancée gérerait aussi le cas d’erreur console.log(error); } }; // on renvoie la valeur du state, et notre fonction setValue return [storedValue, setValue]; }