Retour au cours

frontend / react

useRef

Leçon 81 exercice

Explication

Ce que vous allez apprendre

  • Créer une ref avec useRef et l'attacher à un élément du DOM
  • Expliquer pourquoi modifier une ref ne déclenche jamais de re-rendu
  • Stocker une valeur qui doit persister entre les rendus sans influencer l'affichage
  • Éviter le piège de lire une ref pendant le rendu pour calculer un affichage
  • Exposer une API restreinte d'un composant enfant avec forwardRef et useImperativeHandle

Dans quel contexte ?

Dans src/components/SearchInput.jsx, un développeur veut donner automatiquement le focus au champ de recherche dès que la modale s'ouvre. Utiliser un state pour ça déclencherait un re-rendu inutile à chaque frappe si on s'y prenait mal ; une ref attachée à l'input, combinée à inputRef.current.focus() dans un useEffect, résout le problème sans jamais provoquer de rendu superflu.

Étape 1 : une boîte mutable qui persiste

Une ref est une sorte de boîte mutable que React fait persister entre les rendus d'un composant, sans jamais déclencher de nouveau rendu quand son contenu change.

Étape 2 : la différence fondamentale avec le state

Modifier un state provoque un nouveau rendu pour rafraîchir l'affichage, alors que modifier une ref se fait silencieusement, en coulisses, sans que l'interface ne réagisse.

Étape 3 : le premier besoin, accéder au DOM

Deux besoins distincts justifient l'existence des refs. Le premier est l'accès direct à un élément du DOM réel : donner le focus à un champ, mesurer la taille d'un élément.

Étape 4 : le second besoin, une mémoire hors rendu

Le second besoin est de conserver une valeur entre les rendus sans qu'elle fasse partie de ce qui doit rafraîchir l'affichage : un identifiant de minuteur, un compteur de tentatives.

Étape 5 : comment useRef fonctionne

useRef(valeurInitiale) retourne un objet unique et stable { current: valeurInitiale }, qui reste le même objet à travers tous les rendus. Attaché à un élément JSX via ref, React le remplit avec le noeud DOM une fois monté.

Étape 6 : le piège le plus important

Il reste un piège crucial : une ref lue PENDANT le rendu peut être en retard d'un cycle, puisqu'elle n'est pas synchronisée avec le rendu comme l'est le state. Ne jamais s'en servir pour calculer quelque chose qui doit s'afficher.

Piège fréquent

return <p>{compteRef.current}</p>; n'affiche jamais la valeur à jour : modifier compteRef.current ne déclenche aucun re-rendu, donc l'écran ne se rafraîchit tout simplement pas tant qu'un autre événement (comme un changement de state) ne force pas un nouveau rendu.

BesoinOutilDéclenche un re-rendu ?
Afficher une valeur qui changeuseStateOui
Accéder à un noeud DOM (focus, mesure)useRefNon
Stocker un id de timer, une valeur précédenteuseRefNon

Et ensuite ?

forwardRef sert à transmettre une ref à travers un composant personnalisé, ce qui n'est pas automatique par défaut. Une fois les refs comprises, la prochaine étape aborde l'optimisation des calculs et des fonctions avec useMemo et useCallback.

Commandes & code

useRef

Une boîte mutable qui survit aux rendus sans jamais en déclencher un nouveau.

jsx
import { useRef } from "react";

function ChampAutoFocus() {
    const inputRef = useRef(null);

    useEffect(() => {
        inputRef.current.focus(); // accès direct au noeud DOM réel
    }, []);

    return <input ref={inputRef} />;
}
jsx
// useRef vs useState : la différence fondamentale
function Difference() {
    const [compteState, setCompteState] = useState(0);
    const compteRef = useRef(0);

    function incrementerState() {
        setCompteState((c) => c + 1); // déclenche un nouveau rendu, l'UI se met à jour
    }

    function incrementerRef() {
        compteRef.current += 1; // ne déclenche AUCUN rendu, l'UI ne bouge pas
        console.log("Ref actuelle :", compteRef.current); // valeur bien à jour, juste pas affichée
    }

    return (
        <div>
            <p>State : {compteState}</p>
            <button onClick={incrementerState}>+1 (state)</button>
            <button onClick={incrementerRef}>+1 (ref, invisible)</button>
        </div>
    );
}
jsx
// Cas d'usage réel : conserver une valeur entre rendus sans re-render (ex: id de timer, valeur précédente)
function useValeurPrecedente(valeur) {
    const ref = useRef();

    useEffect(() => {
        ref.current = valeur; // mis à jour APRÈS le rendu, donc "en retard" d'un cycle
    }, [valeur]);

    return ref.current; // pendant le rendu courant, contient encore la valeur du rendu précédent
}

function Compteur() {
    const [compte, setCompte] = useState(0);
    const precedent = useValeurPrecedente(compte);

    return (
        <p>Actuel : {compte}, précédent : {precedent ?? "aucun"}</p>
    );
}
jsx
// forwardRef : exposer un ref à travers un composant custom
import { forwardRef, useImperativeHandle } from "react";

const ChampPersonnalise = forwardRef(function ChampPersonnalise(props, ref) {
    const inputRef = useRef(null);

    // useImperativeHandle : expose une API restreinte au parent, plutôt que le DOM node entier
    useImperativeHandle(ref, () => ({
        focus: () => inputRef.current.focus(),
        vider: () => { inputRef.current.value = ""; },
    }));

    return <input ref={inputRef} {...props} />;
});

function Formulaire() {
    const champRef = useRef(null);

    return (
        <>
            <ChampPersonnalise ref={champRef} />
            <button onClick={() => champRef.current.vider()}>Vider</button>
        </>
    );
}

Résumé

  • Muter ref.current ne déclenche jamais de re-rendu, contrairement à setState.
  • useRef sert autant à référencer un noeud DOM qu'à stocker une valeur mutable "hors rendu".
  • Une ref lue pendant le rendu peut être en retard d'un cycle : ne jamais l'utiliser pour du calcul d'affichage.
  • useImperativeHandle restreint volontairement l'API exposée par un composant à son parent via ref.

Exercices pratiques

1 disponible
1

Mission : l'affichage figé du compteur de tentatives

Objectif : Diagnostiquer pourquoi un compteur stocké dans une ref n'apparaît jamais à l'écran, et exposer une API restreinte via forwardRef.

Contexte

Sur un formulaire de connexion, un développeur veut compter le nombre de tentatives échouées sans provoquer de re-rendu inutile à chaque essai, uniquement pour bloquer le bouton après 5 essais. Il écrit const tentativesRef = useRef(0); puis tentativesRef.current += 1; dans le handler d'échec, et affiche <p>Tentatives : {tentativesRef.current}</p> dans le JSX — le nombre reste bloqué à 0 à l'écran, même après plusieurs échecs visibles dans les logs console.log.

Résoudre l’exercice →