Hooks personnalisés
Ranger la logique réutilisable dans ses propres hooks, pour des composants courts et lisibles.
Objectifs de la séance
- comprendre ce qu'est un hook personnalisé et quand en écrire un ;
- respecter les règles des hooks ;
- extraire
useQuestions,useChronometreetuseTitreDocument; - typer la valeur renvoyée par un hook ;
- alléger
AppetChronometre.
Petite vérification
Avant de commencer, vérifiez si vous savez...
- renommer temporairement
public/questions.jsonet lire le message affiché par l'application - cliquer sur « Réessayer » après avoir remis le bon nom de fichier, et dire ce qui s'affiche
- dire pourquoi la fonction
chargerQuestionsreçoit lesignalen paramètre
📌 Réponse attendue
Notions théoriques
Un hook personnalisé, c'est une fonction qui commence par use
App contient maintenant beaucoup de code de chargement : deux états, un effet, une fonction. Ce code n'a rien à voir avec l'affichage, et un autre composant pourrait en avoir besoin.
Un hook personnalisé est une fonction normale dont le nom commence par use, et qui appelle d'autres hooks (useState, useEffect...). Elle permet de partager de la logique entre composants, sans copier-coller.
export function useQuestions() {
const [etat, setEtat] = useState(...);
useEffect(...);
return { etat, recharger };
}
Les règles des hooks
- un hook s'appelle tout en haut d'un composant ou d'un autre hook, jamais dans un
if, une boucle ou une fonction imbriquée ; - un hook s'appelle uniquement dans un composant ou dans un autre hook (pas dans une fonction ordinaire) ;
- le nom d'un hook personnalisé commence par
use: c'est ce qui permet ànpm run lintde vérifier les deux premières règles.
Un hook partage la logique, pas l'état
Chaque composant qui appelle useChronometre() obtient son propre compteur, avec ses propres états. Deux chronomètres sont indépendants, comme deux appels d'une même fonction.
Le hook useQuestions
On déplace dans un hook tout ce qui concerne le chargement, et on renvoie ce dont le composant a besoin : l'état du chargement et une fonction pour recharger.
import { useEffect, useState } from "react";
import { chargerQuestions, messageErreur } from "../api/questions";
import type { EtatChargement } from "../types/quiz";
type ResultatUseQuestions = {
etat: EtatChargement;
recharger: () => void;
};
export function useQuestions(): ResultatUseQuestions {
const [etat, setEtat] = useState<EtatChargement>({ statut: "chargement" });
const [tentative, setTentative] = useState(0);
useEffect(() => {
const controleur = new AbortController();
async function charger() {
try {
const questions = await chargerQuestions(controleur.signal);
setEtat({ statut: "succes", questions });
} catch (erreur) {
if (controleur.signal.aborted) {
return;
}
setEtat({ statut: "erreur", message: messageErreur(erreur) });
}
}
charger();
return () => {
controleur.abort();
};
}, [tentative]);
function recharger() {
setEtat({ statut: "chargement" });
setTentative((ancien) => ancien + 1);
}
return { etat, recharger };
}
Le type ResultatUseQuestions décrit ce que le hook renvoie : c'est sa « carte d'identité ». Dans App, l'utilisation tient en une ligne :
const { etat: chargement, recharger } = useQuestions();
Remarquez la destructuration avec renommage : etat: chargement crée une variable chargement à partir de la propriété etat.
Les hooks useChronometre et useTitreDocument
Même principe pour le chronomètre : le composant ne garde que l'affichage.
import { useEffect, useState } from "react";
export function useChronometre(): number {
const [secondes, setSecondes] = useState(0);
useEffect(() => {
const minuterie = setInterval(() => {
setSecondes((ancien) => ancien + 1);
}, 1000);
return () => {
clearInterval(minuterie);
};
}, []);
return secondes;
}
import { useEffect } from "react";
export function useTitreDocument(titre: string): void {
useEffect(() => {
const ancienTitre = document.title;
document.title = titre;
return () => {
document.title = ancienTitre;
};
}, [titre]);
}
useTitreDocument mémorise l'ancien titre et le restaure au nettoyage, au lieu de réécrire en dur "Quiz Cyber" : le hook peut ainsi servir dans n'importe quelle page.
Et le composant devient très court :
import { useChronometre } from "../hooks/useChronometre";
import { useTitreDocument } from "../hooks/useTitreDocument";
import { formaterDuree } from "../logic/duree";
export function Chronometre() {
const secondes = useChronometre();
const duree = formaterDuree(secondes);
useTitreDocument(`Quiz Cyber (${duree})`);
return <p>Temps : {duree}</p>;
}
Créez un hook quand (1) la même logique avec état ou effet est utilisée à plusieurs endroits, ou (2) un composant devient difficile à lire à cause de la logique qu'il contient. Ne créez pas un hook « par principe » pour une ligne.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Votre mission : alléger App et Chronometre en extrayant trois hooks.
Étape 1 — Le hook useQuestions
Créez src/hooks/useQuestions.ts en déplaçant le chargement hors de App. Complétez le type de retour.
Le type de retour est la carte d'identité du hook : en le lisant, on sait ce qu'on peut en tirer, sans lire son code.
Étape 2 — Utiliser useQuestions dans App
Remplacez les deux états et l'effet de chargement par l'appel du hook.
App ne contient plus ni fetch, ni AbortController, ni useEffect : elle décide quel écran afficher, rien d'autre. Supprimez les imports devenus inutiles (npm run lint les signale).
Étape 3 — Le hook useChronometre
Créez src/hooks/useChronometre.ts : le hook renvoie le nombre de secondes écoulées.
En déplaçant l'effet dans un hook, on déplace aussi son nettoyage : la minuterie est toujours arrêtée, quel que soit le composant qui utilise le hook.
Étape 4 — Le hook useTitreDocument
Créez src/hooks/useTitreDocument.ts.
Mémoriser l'ancienne valeur et la restaurer rend le hook indépendant de l'application : il fonctionnera dans n'importe quel projet.
Étape 5 — Un composant Chronometre très court
Réécrivez Chronometre.tsx avec les deux hooks.
Le composant dit ce qu'il fait (« un chronomètre qui met aussi à jour le titre ») sans montrer comment : les détails sont dans les hooks, testés une fois pour toutes.
📌 Une solution
Ce qu'il faut retenir
| Notion | Résumé |
|---|---|
| Hook personnalisé | Fonction dont le nom commence par use et qui appelle d'autres hooks. |
| Règles des hooks | Appel tout en haut, jamais dans un if ou une boucle, uniquement dans un composant ou un hook. |
| Partage | Un hook partage la logique, pas l'état : chaque appel a ses propres états. |
| Type de retour | Un type nommé décrit ce que renvoie le hook. |
| Refactoring | Le comportement ne change pas, le code devient plus lisible et réutilisable. |
Aperçu de la prochaine séance
Votre application a une seule page qui change d'écran grâce à un état. Dans le tutoriel 4, vous allez la transformer en vraie application à plusieurs pages avec React Router : une adresse par écran, un bouton « Précédent » qui fonctionne, et des paramètres d'URL validés.