Aller au contenu principal

React : comprendre les hooks avec une vraie barre de recherche

Tout le monde se sert d'une barre de recherche des dizaines de fois par jour. On tape, les résultats apparaissent, et entre les deux il se passe plus de choses qu'il n'y paraît. Ce composant ordinaire est le meilleur terrain que je connaisse pour comprendre les hooks React : à quoi ils servent, pourquoi ils existent, et comment ils travaillent ensemble sur du code utile.

Guides & tutoriels ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

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.

Questions fréquentes

Quelle différence entre useState et useRef en pratique ?

Modifier un state déclenche un nouveau rendu, ce qui met l'écran à jour. Modifier une ref ne redessine rien. Utilise du state pour tout ce que l'utilisateur doit voir changer, et une ref pour ce que tu veux mémoriser entre les rendus sans affecter l'affichage, comme un identifiant de timer ou une référence vers un élément du DOM.

Pourquoi mon useEffect tourne-t-il en boucle infinie ?

Deux causes courantes. Soit le tableau de dépendances est absent, et l'effet se relance après chaque rendu, y compris ceux qu'il provoque lui-même en modifiant du state. Soit une dépendance change de référence à chaque rendu, comme un objet ou une fonction recréé dans le corps du composant. Vérifie le tableau et stabilise les valeurs qui bougent inutilement.

À quoi sert la fonction retournée dans useEffect ?

C'est la fonction de nettoyage. React l'exécute avant de relancer l'effet et au moment où le composant disparaît. Elle sert à annuler ce que l'effet a lancé: couper une requête réseau, effacer un timer, retirer un écouteur d'événement. Sans elle, tu accumules des ressources actives et tu t'exposes à des bugs de course entre opérations asynchrones.

Un hook maison, c'est compliqué à écrire ?

Non. Un hook maison est une fonction dont le nom commence par use et qui appelle d'autres hooks à l'intérieur. Tu en extrais un dès qu'une logique à base de hooks se répète dans plusieurs composants, comme un debounce ou un accès à une API. Cela évite le copier-coller et rend la logique testable séparément.

Peut-on appeler un hook dans une condition ?

Non, jamais. React associe chaque hook à sa mémoire grâce à l'ordre d'appel, qui doit rester identique d'un rendu à l'autre. Placer un hook dans un if, une boucle ou après un return anticipé change cet ordre et fait planter le composant. Les hooks s'écrivent toujours au sommet de la fonction. Mets plutôt la condition à l'intérieur du hook.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous