Avant d'entrer dans le sujet, un mot sur la façon dont React s'enseigne d'habitude. La plupart des cours s'ouvrent sur le même exercice: un compteur. Un bouton, un chiffre affiché, et à chaque clic le chiffre monte d'un cran. L'exercice tient en trois lignes et montre la syntaxe du premier hook. Le souci, c'est qu'il s'arrête là. Un compteur tient dans une seule variable, ne parle à personne, ne charge rien. Or dans un composant qui sert à quelque chose, un hook coordonne plusieurs morceaux d'état qui changent à des moments différents, déclenche des effets de bord au bon instant, et nettoie derrière lui quand le composant disparaît. Rien de tout ça n'apparaît sur un +1.
On va donc construire une barre de recherche qui interroge l'API publique de GitHub et affiche les dépôts trouvés. Ce composant tient sur une page, mais il fait travailler quatre hooks ensemble: useState, useEffect, useRef, et un hook maison. Si tu débutes en React, la maîtrise de JavaScript aide beaucoup ici, parce que la moitié des difficultés viennent de fonctions, de fermetures et d'asynchrone, pas de React lui-même.
Le problème que le compteur cache
Prends la barre de recherche. L'utilisateur tape, et à chaque frappe plusieurs choses doivent bouger. Le texte saisi change. Les résultats affichés changent, mais avec un décalage, parce qu'on ne veut pas envoyer une requête à chaque lettre. Un indicateur de chargement apparaît puis disparaît. Une erreur peut surgir si le réseau tombe. Et si l'utilisateur tape vite, deux requêtes partent, la première revient après la seconde, et l'écran affiche un résultat périmé.
Ce dernier point mérite qu'on s'y arrête, parce qu'il casse des applications en production tous les jours. Tu tapes "re", une requête part. Tu ajoutes "act", une seconde requête part. Le serveur est capricieux, la réponse pour "re" arrive après celle pour "react". Sans précaution, ton composant affiche les dépôts de "re" alors que le champ contient "react". L'utilisateur voit des résultats qui ne correspondent pas à ce qu'il lit à l'écran.
Il y a aussi la question du rythme. Sur un compteur, l'utilisateur clique, l'état change, l'écran suit. Un pas après l'autre, sans surprise. Dans une recherche, les événements ne sont plus synchrones: la frappe arrive maintenant, la réponse réseau arrive plus tard, et entre les deux le composant a pu se réafficher plusieurs fois pour d'autres raisons. Tu dois raisonner sur des choses qui se produisent dans un ordre que tu ne contrôles pas entièrement. C'est un saut mental que le compteur n'exige jamais.
Un compteur ne t'apprend aucun de ces réflexes. Un composant de recherche te les impose. C'est pour ça qu'il fait un bien meilleur terrain d'entraînement.
Le principe: un hook est une mémoire attachée à un rendu
Un composant React est une fonction qui s'exécute encore et encore. À chaque changement, React rappelle la fonction du début, elle recalcule tout, et React compare le résultat avec l'affichage précédent pour mettre à jour l'écran. Une fonction normale oublie tout entre deux appels. Un hook est le mécanisme qui donne de la mémoire à cette fonction: entre deux exécutions, il conserve une valeur.
Chaque hook joue un rôle distinct dans cette mémoire. useState garde une valeur et, quand tu la modifies, demande à React de refaire un rendu. useEffect exécute du code après le rendu, quand certaines valeurs ont changé, et sait nettoyer ses propres traces. useRef garde aussi une valeur entre les rendus, mais la modifier ne déclenche aucun nouveau rendu, ce qui en fait l'endroit idéal pour ranger un identifiant de timer ou une référence vers un élément du DOM.
La règle mentale qui débloque beaucoup de monde: si une valeur doit rester à l'écran après un rendu et déclencher une mise à jour visible quand elle change, c'est du state. Si elle doit survivre entre les rendus sans jamais provoquer de réaffichage, c'est une ref.
Cette distinction n'est pas un détail. Elle sépare le développeur qui range chaque donnée au bon endroit de celui qui met tout dans du state et se demande pourquoi son composant se réaffiche vingt fois par seconde.
useState: l'état visible du composant
On commence par ce que l'utilisateur voit changer. Le texte tapé, les résultats, le fait qu'une requête soit en cours, et une éventuelle erreur. Chacune de ces données mérite son propre appel à useState, parce que ce sont des choses indépendantes qui évoluent séparément.
function RechercheDepots() {
const [saisie, setSaisie] = useState("");
const [resultats, setResultats] = useState([]);
const [chargement, setChargement] = useState(false);
const [erreur, setErreur] = useState(null);
return (
<div>
<input
value={saisie}
onChange={(e) => setSaisie(e.target.value)}
placeholder="Cherche un dépôt GitHub"
/>
{chargement && <p>Recherche en cours...</p>}
{erreur && <p>Une erreur est survenue.</p>}
<ul>
{resultats.map((depot) => (
<li key={depot.id}>{depot.full_name}</li>
))}
</ul>
</div>
);
}
Le champ de saisie est ce qu'on appelle un composant contrôlé: sa valeur affichée vient du state, et chaque frappe passe par setSaisie. React redessine, l'input montre la nouvelle valeur. C'est le même mécanisme que le compteur, sauf qu'ici il pilote un vrai formulaire.
Une chose à noter tout de suite: setSaisie ne modifie pas la variable sur-le-champ. Elle programme un nouveau rendu. Dans la même exécution de la fonction, saisie garde son ancienne valeur jusqu'au prochain passage. Ce point piège beaucoup de gens venus d'autres langages où une affectation est immédiate.
Un hook maison pour retarder la requête
Envoyer une requête à chaque lettre serait un gâchis et ferait clignoter l'interface. On veut attendre que l'utilisateur ait fini de taper, disons 400 millisecondes de silence, avant de lancer la recherche. Ce comportement s'appelle le debounce, et c'est le candidat parfait pour un hook maison, parce qu'on va vouloir le réutiliser ailleurs.
function useDebounce(valeur, delai) {
const [valeurRetardee, setValeurRetardee] = useState(valeur);
useEffect(() => {
const timer = setTimeout(() => {
setValeurRetardee(valeur);
}, delai);
return () => clearTimeout(timer);
}, [valeur, delai]);
return valeurRetardee;
}
Regarde le nettoyage: la fonction retournée par useEffect annule le timer précédent. Tant que l'utilisateur tape, chaque frappe programme un nouveau timer et efface l'ancien, donc rien ne se déclenche. Dès qu'il arrête pendant 400 millisecondes, le dernier timer arrive au bout et met à jour la valeur retardée. Un hook maison n'a rien de magique: c'est une fonction dont le nom commence par use et qui appelle d'autres hooks. Cette convention permet à React de vérifier que tu respectes ses règles.
Dans le composant, on branche ce hook en une ligne, et saisieRetardee ne change que quand la saisie se stabilise.
const saisieRetardee = useDebounce(saisie, 400);
useEffect: parler au monde extérieur
Le rendu d'un composant doit rester pur: il calcule un affichage à partir du state, sans effet de bord. Un appel réseau est un effet de bord par excellence. Sa place est dans useEffect, qui s'exécute après le rendu et se relance quand ses dépendances changent. Ici, la dépendance est la saisie retardée.
useEffect(() => {
if (saisieRetardee.trim() === "") {
setResultats([]);
return;
}
const controleur = new AbortController();
setChargement(true);
setErreur(null);
fetch(
`https://api.github.com/search/repositories?q=${saisieRetardee}`,
{ signal: controleur.signal }
)
.then((r) => r.json())
.then((data) => {
setResultats(data.items ?? []);
setChargement(false);
})
.catch((err) => {
if (err.name === "AbortError") return;
setErreur(err);
setChargement(false);
});
return () => controleur.abort();
}, [saisieRetardee]);
C'est ici que le composant devient sérieux. L'AbortController et la fonction de nettoyage règlent le fameux problème de course entre requêtes. Quand la saisie retardée change, React exécute d'abord le nettoyage de l'effet précédent, qui annule la requête en vol, puis lance le nouvel effet. La réponse périmée n'a plus aucune chance d'écraser la bonne, parce que sa requête a été coupée avant de revenir.
Le tableau de dépendances [saisieRetardee] dit à React quand relancer l'effet. Oublie-le, et l'effet tourne après chaque rendu, y compris ceux provoqués par setResultats, ce qui crée une boucle qui martèle l'API GitHub jusqu'à te faire bloquer.
Un mot sur l'ordre d'exécution, parce qu'il déroute au début. useEffect ne s'exécute pas pendant le rendu, mais juste après que React a mis à jour l'écran. Le composant s'affiche donc une première fois avec les anciens résultats et l'indicateur de chargement, et seulement ensuite la requête part. Ce décalage d'un temps est voulu: il garde le rendu rapide et prévisible, et déporte tout ce qui touche au monde extérieur dans une phase séparée où il ne bloque pas l'affichage.
useRef: garder une valeur sans réafficher
Ajoutons un besoin réaliste: placer le curseur dans le champ de recherche dès que la page s'ouvre, pour que l'utilisateur puisse taper sans cliquer. Il faut une référence vers l'élément input du DOM. Ce n'est pas du state, parce que cette référence ne change jamais et ne doit provoquer aucun réaffichage.
const champRef = useRef(null);
useEffect(() => {
champRef.current?.focus();
}, []);
// dans le JSX
<input ref={champRef} value={saisie} ... />
Le tableau de dépendances vide [] signifie que cet effet ne s'exécute qu'une fois, au montage du composant. React remplit champRef.current avec le vrai élément input, et on appelle sa méthode focus native.
useRef sert aussi à ranger des valeurs de travail qui n'ont rien à faire dans l'affichage. Un compteur de requêtes envoyées, l'identifiant d'un intervalle, la valeur précédente d'une prop pour la comparer. À chaque fois que tu te dis "j'ai besoin de me souvenir de ça mais l'écran s'en fiche", pense à useRef avant de sortir un useState qui déclencherait des rendus inutiles.
Les pièges qui reviennent le plus souvent
Le premier, tu l'as déjà croisé plus haut: le tableau de dépendances mal rempli. Trop peu de dépendances, et ton effet utilise des valeurs périmées. Trop, ou une valeur qui change à chaque rendu, et il se relance en boucle. Le linter officiel de React, la règle exhaustive-deps, repère la plupart de ces erreurs. Installe-le et écoute-le, même quand il te dérange.
Le deuxième piège touche la mutation du state. Si tes résultats étaient dans un objet et que tu écrivais resultats.push(nouveau) avant d'appeler le setter avec le même tableau, React ne verrait aucun changement de référence et ne redessinerait pas. Il faut toujours créer une nouvelle valeur, par exemple setResultats([...resultats, nouveau]). React compare les références, pas le contenu.
Le troisième concerne l'ordre des hooks. React se repère à l'ordre d'appel pour associer chaque hook à sa mémoire. Appeler un hook dans un if, une boucle ou après un return anticipé casse cet ordre d'un rendu à l'autre, et l'application plante. Les hooks vivent toujours au sommet de la fonction, dans le même ordre à chaque passage.
Le dernier est moins une erreur qu'une confusion. Beaucoup rangent dans du state des choses qui n'ont pas besoin de déclencher de rendu, et se retrouvent avec des composants qui se réaffichent sans raison visible. La question à te poser reste la même à chaque déclaration: est-ce que l'écran doit réagir quand cette valeur change?
Ce que ce composant t'a appris que le compteur ne pouvait pas
En une soixantaine de lignes, tu as vu du state coordonné, un effet de bord asynchrone, un nettoyage qui règle un vrai bug de production, un hook maison réutilisable, et une référence DOM. C'est exactement la matière que tu retrouveras dans un composant de vraie application, formulaire, tableau de bord, ou vue de détail.
Un dernier point, propre à 2026. Les assistants IA écrivent ce composant en dix secondes, nettoyage compris. Ça ne rend pas les hooks moins utiles à comprendre, au contraire: en entretien comme en revue de code, la question n'est plus "sais-tu l'écrire" mais "sais-tu dire pourquoi il tient debout, et ce qui casse si on enlève cette ligne". L'article sur le vibecoding et le poste de dev junior détaille ce déplacement de la valeur.
Le meilleur moyen d'ancrer tout ça reste de taper le code toi-même et de le casser volontairement: enlève le nettoyage et tape vite pour voir la course apparaître, retire le tableau de dépendances pour observer la boucle. Si tu veux structurer cet apprentissage plutôt que d'accumuler des tutoriels isolés, notre formation React sur des interfaces réelles part de ce genre de composant, et l'article sur le syndrome du tutoriel infini explique pourquoi passer à la construction change tout. Une fois React en main, TypeScript rend ces mêmes hooks beaucoup plus sûrs à manipuler.