Aller au contenu principal

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, useChronometre et useTitreDocument ;
  • typer la valeur renvoyée par un hook ;
  • alléger App et Chronometre.

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. renommer temporairement public/questions.json et lire le message affiché par l'application
  2. cliquer sur « Réessayer » après avoir remis le bon nom de fichier, et dire ce qui s'affiche
  3. dire pourquoi la fonction chargerQuestions reçoit le signal en 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​

  1. 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 ;
  2. un hook s'appelle uniquement dans un composant ou dans un autre hook (pas dans une fonction ordinaire) ;
  3. le nom d'un hook personnalisé commence par use : c'est ce qui permet à npm run lint de 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.

src/hooks/useQuestions.ts
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.

src/hooks/useChronometre.ts
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;
}
src/hooks/useTitreDocument.ts
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 :

src/components/Chronometre.tsx
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>;
}
Bonne pratique - Quand créer un hook ?

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​


Qu'est-ce qu'un hook personnalisé ?


Que partagent deux composants qui appellent chacun useChronometre() ?


Où est-il interdit d'appeler un hook ?


Que signifie const { etat: chargement, recharger } = useQuestions(); ?


Pourquoi useTitreDocument mémorise-t-il l'ancien titre ?


Dans quel cas est-il pertinent de créer un hook personnalisé ?



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.


Bonne pratique - Typer ce que renvoie un hook

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.


Bonne pratique - App se concentre sur l'affichage

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.


Bonne pratique - Le nettoyage voyage avec l'effet

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.


Bonne pratique - Restaurer plutôt que réécrire

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.


Bonne pratique - Un composant lisible en dix secondes

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​

NotionRésumé
Hook personnaliséFonction dont le nom commence par use et qui appelle d'autres hooks.
Règles des hooksAppel tout en haut, jamais dans un if ou une boucle, uniquement dans un composant ou un hook.
PartageUn hook partage la logique, pas l'état : chaque appel a ses propres états.
Type de retourUn type nommé décrit ce que renvoie le hook.
RefactoringLe 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.