Dans cet article, on ne va pas apprendre à utiliser des Hooks existants, on va en créer de nouveaux.
Le fait de pouvoir créer une logique et de la réutiliser dans plusieurs composant est l’essence même de cette fonctionnalité.
On va voir comment ça fonctionne, quelle sont les règles à respecter et on en fera quelques exemples. N’hésitez pas à revoir mes deux articles précédents (Les Hooks - La base / Les Hooks - Les cas particuliers). J’utiliserai les Hooks présentez dans ceux-ci.
Concept
Vous avez sûrement déjà fait des Hooks personnalisés, consciemment ou inconsciemment. Lorsque vous créez un composant avec plusieurs Hooks, vous créez une logique qui peut être extraite de votre composant.
Prenons le cas suivant :
function PokemonDetails({ pokemonId }) {
const [pokemon, setPokemon] = useState(null);
const updatePokemonData = async () => {
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${pokemonId}`
);
const pokemon = await response.json();
setPokemon(pokemon);
};
useEffect(() => {
updatePokemonData();
}, [pokemonId]);
return <span> {pokemon?.name || ""} </span>;
}
Plutôt simple, non ? On peut extraire cette logique en Hook personnalisé.
Réfléchissons ! Notre composant a besoin d’un pokemon
. On va donc créer un Hook qui retourne un pokemon
. On le nommera usePokemon
. Il faut aussi lui fournir la propriété pokemonId
.
function ItemDetail({ pokemonId }) {
const pokemon = useItem(pokemonId);
return <span> {pokemon?.name || ""} </span>;
}
C’est déjà plus compréhensible. Mais maintenant, on va implémenter usePokemon
.
Qu’est-ce qu’un Hook à la base ? Une fonction !
// On n'oublie pas de passer l'id qui nous intéresse
function usePokemon(pokemonId) {}
C’est un bon début. On va reprendre toute la logique du composant PokemonDetails
.
function usePokemon(pokemonId) {
const [pokemon, setPokemon] = useState(null);
const updatePokemonData = async () => {
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${pokemonId}`
);
const pokemon = await response.json();
setPokemon(pokemon);
};
useEffect(() => {
updatePokemonData();
}, [pokemonId]);
}
J’ai juste copié-collé la totalité du composant, hormis ce qu’il retournait. Il ne manque plus que la touche finale ! Notre Hook est censé retourner un pokemon
. Il faut donc retourner celui qui est mis à jour dans cette fonction.
function usePokemon(pokemonId) {
const [pokemon, setPokemon] = useState(null);
const updatePokemonData = async () => {
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${pokemonId}`
);
const pokemon = await response.json();
setPokemon(pokemon);
};
useEffect(() => {
updatePokemonData();
}, [pokemonId]);
return pokemon;
}
Tadaaa ! On a maintenant un Hook qui va aller récupérer les données d’un pokemon
au montage du composant et qui rafraîchira ces données si l’id change. Libre à vous d’améliorer ce Hook comme vous l’entendez. Vous pouvez ajouter la gestion d’erreur, la gestion du chargement ou encore ajouter une variable model
qui permettra de récupérer les données d’un scope plus large.
Règles
Il n’y a pas vraiment de règle particulière pour les Hooks personnalisés. Ce sont les mêmes que pour les Hooks fournies par React : appeler les Hooks uniquement dans un composant fonctionnel React et garder le même ordre d’appel.
La convention veut que le nom des Hooks personnalisés commence par use
. Cela permet de les reconnaître facilement et de les gérer comme tel via les règles Eslint.
Et tout comme les Hooks standards, l’appel à un Hook personnalisé est isolé (à sa propre instance), peut être utilisé plusieurs fois dans un même composant et même être utilisé dans un autre Hook personnalisé.
Exemples
Voici quelques exemples de Hooks personnalisés qui m’ont servi (peut-être vous aussi) et qui vous montreront quelques implémentations.
useFetchJson
En parlant de fetch
, voici un Hook qui va permettre de gérer le “cycle de vie”. On va retrouver la gestion des erreurs et du cycle de chargement.
function useFetchJson(params) {
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const [pending, setPending] = useState(false);
useEffect(() => {
async function doFetch() {
setPending(true);
try {
const response = await fetch(params);
setData(await response.json());
} catch (err) {
setError(err);
} finally {
setPending(false);
}
}
if (params && !pending) {
doFetch();
}
}, [params]);
return [data, pending, error];
}
On va pouvoir utiliser de la manière suivante.
function Page() {
const [data, pending, error] = useFetchJson("url");
if (pending) return <p>Chargement en cours...</p>;
if (error) return <p>{error.message}</p>;
if (data) return <p>{data.info}</p>;
return <p> Le chargement des données va bientôt commencer </p>;
}
L’utilisation de useState
et useEffect
ensemble est le plus commun. C’est la représentation parfaite de la capture d’une logique. On utilise un état modifié par un effet personnalisé.
Ce Hook est un exemple, vous pouvez le reprendre tel quel, le modifier selon vos besoins ou refaire le vôtre. Je vous encourage d’ailleurs à effectuer ce dernier choix. 😉
useModal
Un Hook intéressant si vous ne savez pas trop comment gérer vos modales dans votre application. Régulièrement, je n’en utilise qu’une seul que je contrôle de partout.
import React, { useReducer, useContext, createContext } from "React";
// Valeur initiale de l'état de la modal
const modalInitialState = { open: false, text: "" };
// Initialisation du contexte de la modal
const ModalContext = React.createContext([modalInitialState, () => {}]);
// La fonction reducer
const modalReducer = (state, action) => {
switch (action.type) {
case "open":
return { text: action.text, open: true };
case "close":
return { text: "", open: false };
default:
return state;
}
};
// Le composant Modal à afficher
const Modal = ({ open, text }) => {
return open ? <div>{text}</div> : null;
};
// Le composant Provider qui va nous permettre d'afficher la modal
// et fournir aux enfants la possibilité de contrôler la modal
export const ModalProvider = ({ children }) => {
// Initialisation du useReducer
const modal = useReducer(modalReducer, modalInitialState);
const { open, text } = modal[0];
return (
<ModalContext.Provider value={modal}>
<Modal open={open} text={text} />
{children}
</ModalContext.Provider>
);
};
// Export du useContext sous forme useModal
export const useModal = () => useContext(ModalContext);
On va pouvoir utiliser de la manière suivante.
import React from 'react'
// Import de notre Hook
import {useModal, ModalProvider} from './useModal'
function Page() {
// Le composant Page est un enfant du ModalProvider
// et peux donc utiliser useModal correctement
const [modalState, dispatchModal] = useModal();
const handleModal = () => {
const modalAction = modalState.open
? { type: "close" }
: { type: "open", text: "coucou" };
// On utilise dispatchModal pour mettre à jour la modal
dispatchModal(modalAction);
};
return <button onClick={handleModal}>{modalState.open ? 'Close' : 'Open'} Modal</button>;
};
function App() {
return (
{/* Le composant ModalProvider doit être placé de sorte que ces sous composants puissent utiliser la modal*/}
<ModalProvider>
<Page />
</ModalProvider>
);
}
On utilise useReducer
et useContext
ensemble pour avoir un environnement accessible et modifiable de n’importe où. Ici, c’est pour une modal mais vous pouvez l’utiliser pour d’autres types de composants.
Conclusion
La création de Hooks personnalisés n’est pas vraiment une fonctionnalité en soi. C’est plutôt la direction naturelle qui résulte de l’écosystème de React et la fonctionnalité des Hooks.
Plus vous allez les utiliser, plus vous aurez l’opportunité de créer vos propre Hooks. Certains seront propres au projet ou à la situation, pendant que d’autres resteront dans votre mallette à outils, s’améliorant au fil du temps.
Comme toujours, l’exercice est la clé du succès. Mais pour cet article, je ne voyais pas trop le besoin… Vous savez dorénavant utiliser les Hooks. Alors utilisez-les comme vous le sentez. Créez votre propre Hook !
N’hésitez pas à nous le partager sur Twitter (@rednetio) si vous êtes fier de votre création ou si vous avez une meilleure option pour ceux présentés en exemple.