frontend / react
useRef
Explication
Ce que vous allez apprendre
- Créer une ref avec
useRefet 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
forwardRefetuseImperativeHandle
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.
| Besoin | Outil | Déclenche un re-rendu ? |
|---|---|---|
| Afficher une valeur qui change | useState | Oui |
| Accéder à un noeud DOM (focus, mesure) | useRef | Non |
| Stocker un id de timer, une valeur précédente | useRef | Non |
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.
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} />;
}// 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>
);
}// 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>
);
}// 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.currentne déclenche jamais de re-rendu, contrairement àsetState. useRefsert 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.
useImperativeHandlerestreint volontairement l'API exposée par un composant à son parent viaref.
Exercices pratiques
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.