Référencer des valeurs avec les refs

Lorsque vous souhaitez que votre composant « se souvienne » de quelque chose, mais que vous voulez éviter que l’évolution de ces données déclenche de nouveaux rendus, vous pouvez utiliser une ref.

Vous allez apprendre

  • Comment ajouter une ref à votre composant
  • Comment mettre à jour la valeur d’une ref
  • En quoi les refs diffèrent des variables d’état
  • Comment utiliser les refs de façon fiable

Ajouter une ref à votre composant

Vous pouvez ajouter une ref à votre composant en important le Hook useRef de React :

import { useRef } from 'react';

Au sein de votre composant, appelez le Hook useRef et passez-lui comme unique argument la valeur initiale que vous souhaitez référencer. Par exemple, voici une ref vers la valeur 0 :

const ref = useRef(0);

useRef renvoie un objet comme celui-ci :

{
current: 0 // La valeur que vous avez passée à useRef
}
Une flèche labellisée “current” au sein d'une poche avec “ref” écrit dessus.

Illustré par Rachel Lee Nabors

Vous pouvez accéder à la valeur actuelle de cette ref au travers de la propriété ref.current. Cette valeur est volontairement modifiable, ce qui signifie que vous pouvez aussi bien la lire que l’écrire. C’est un peu comme une poche secrète de votre composant que React ne peut pas surveiller. (C’est ce qui en fait une « échappatoire » du flux de données unidirectionnel de React—on va détailler ça dans un instant !)

Voici maintenant un bouton qui incrémente ref.current à chaque clic :

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('Vous avez cliqué ' + ref.current + ' fois !');
  }

  return (
    <button onClick={handleClick}>
      Cliquez ici
    </button>
  );
}

La ref pointe vers un nombre, mais tout comme pour l’état, vous pouvez pointer vers ce que vous voulez : une chaîne de caractères, un objet, ou même une fonction. Contrairement aux variables d’état, une ref est juste un objet JavaScript brut avec une propriété current que vous pouvez lire et modifier.

Remarquez que le composant ne refait pas de rendu à chaque incrémentation. Comme les variables d’état, les refs sont préservées par React d’un rendu à l’autre. Cependant, modifier un état entraîne un nouveau rendu du composant, tandis que modifier une ref ne le fait pas !

Exemple : construire un chronomètre

Vous pouvez combiner des refs et des variables d’état dans un même composant. Construisons par exemple un chronomètre que l’utilisateur peut démarrer et arrêter en pressant un bouton. Afin de pouvoir afficher le temps écoulé depuis que l’utilisateur a pressé « Démarrer », vous allez devoir garder trace du moment auquel ce bouton a été pressé, et du moment courant. Ces informations sont nécessaires au rendu, de sorte que vous les stockez dans des variables d’état :

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

Lorsque l’utilisateur pressera « Démarrer », vous utiliserez setInterval afin de mettre à jour le moment courant toutes les 10 millisecondes :

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);

  function handleStart() {
    // Commencer à chronométrer.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Mettre à jour le temps toutes les 10ms.
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Temps écoulé : {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Démarrer
      </button>
    </>
  );
}

Lorsque le bouton « Arrêter » est pressé, vous devez annuler l’intervalle pour qu’il cesse de mettre à jour la variable d’état now. Vous pouvez faire ça en appelant clearInterval, mais vous avez besoin de lui fournir l’ID du timer renvoyé précédemment par votre appel à setInterval lorsque l’utilisateur avait pressé « Démarrer ». Il faut donc bien conserver cet ID quelque part. Puisque l’ID de l’intervalle n’est pas nécessaire au rendu, vous pouvez le conserver dans une ref :

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Temps écoulé : {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Démarrer
      </button>
      <button onClick={handleStop}>
        Arrêter
      </button>
    </>
  );
}

Lorsqu’une information est utilisée pour le rendu, conservez-la dans une variable d’état. Lorsqu’elle n’est nécessaire qu’aux gestionnaires d’événements, et que la modifier ne doit pas entraîner un nouveau rendu, il est plus efficace de passer par une ref.

Différences entre refs et variables d’état

Vous trouvez peut-être que les refs semblent moins « strictes » que les variables d’état—vous pouvez les modifier plutôt que de devoir passer par une fonction de mise à jour d’état, par exemple. Pourtant dans la majorité des cas, vous voudrez utiliser des états. Les refs sont une « échappatoire » dont vous n’aurez pas souvent besoin. Voici un comparatif entre variables d’état et refs :

RefVariable d’état
useRef(initialValue) renvoie { current: initialValue }.useState(initialValue) renvoie la valeur actuelle d’une variable d’état et une fonction de modification de cette valeur ([value, setValue]).
Ne redéclenche pas un rendu quand vous la modifiez.Déclenche un nouveau rendu quand vous la modifiez.
Modifiable : vous pouvez changer la valeur de current hors du rendu.« Immuable » : vous devez passer par la fonction de mise à jour de l’état pour changer la variable d’état, ce qui est mis en attente pour le prochain rendu.
Vous ne devriez pas lire (ou écrire) la valeur de current pendant le rendu.Vous pouvez lire l’état à tout moment. En revanche, chaque rendu a son propre instantané de l’état, qui ne change pas.

Voici un bouton de compteur implémenté avec une variable d’état :

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Vous avez cliqué {count} fois
    </button>
  );
}

Étant donné que la valeur count est affichée, il est logique d’utiliser une variable d’état pour la stocker. Quand la valeur du compteur est modifiée via setCount(), React fait un nouveau rendu du composant et l’écran est mis à jour pour refléter le nouveau compteur.

Si vous utilisiez une ref, React ne déclencherait jamais un nouveau rendu du composant, et vous ne verriez jamais la valeur changer ! Essayez ci-dessous de cliquer le bouton, ça ne met pas à jour le texte :

import { useRef } from 'react';

export default function Counter() {
  let countRef = useRef(0);

  function handleClick() {
    // Le composant ne refait pas son rendu !
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      Vous avez cliqué {countRef.current} fois
    </button>
  );
}

Voilà pourquoi la lecture de ref.current pendant le rendu n’est pas une pratique fiable. Si vous avez besoin de ça, utilisez plutôt une variable d’état.

En détail

Comment fonctionne useRef en interne ?

Même si useState et useRef sont fournis par React, en principe useRef pourrait être implémenté par-dessus useState. On pourrait imaginer que dans le code de React, useRef serait peut-être implémenté de la façon suivante :

// Dans React…
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}

Lors du premier rendu, useRef renvoie { current: initialValue}. Cet objet est stocké par React, de sorte qu’au prochain rendu il renverra le même objet. Remarquez que la fonction de modification de l’état est inutilisée dans ce code. Elle est superflue puisque useRef renvoie toujours le même objet !

React fournit directement useRef parce qu’il s’agit d’un cas d’usage suffisamment courant. Mais vous pouvez le voir comme une variable d’état classique mais sans fonction modificatrice. Si vous avez l’habitude de la programmation orientée objet, les refs vous font peut-être penser à des champs d’instance—sauf qu’au lieu d’écrire this.something vous écrivez somethingRef.current.

Quand utiliser des refs

En général, vous utiliserez une ref lorsque votre composant a besoin de « sortir » de React et communiquer avec des API extérieures (souvent une API du navigateur qui n’impactera pas l’apparence du composant). Voici quelques-unes de ces situations peu fréquentes :

Si votre composant a besoin de stocker une valeur, mais que cette valeur n’impacte pas la logique de rendu, optez pour une ref.

Meilleures pratiques pour les refs

Pour rendre vos composants plus prévisibles, respectez les principes suivants :

  • Traitez les refs comme une échappatoire. Les refs sont utiles lorsque vous travaillez avec des systèmes extérieurs ou des API du navigateur. Mais si une large part de votre logique applicative et de votre flux de données repose sur les refs, vous devriez probablement repenser votre approche.
  • Ne lisez pas et n’écrivez pas dans ref.current pendant le rendu. Si une information est nécessaire au rendu, utilisez plutôt un état. Comme React ne sait pas que ref.current change, même le simple fait de la lire pendant le rendu peut introduire des comportements déroutants dans votre composant. (La seule exception concerne du code du style if (!ref.current) ref.current = new Thing(), qui ne met à jour la ref qu’une fois lors du rendu initial.)

Les contraintes des états React ne s’appliquent pas aux refs. Par exemple, l’état se comporte comme un instantané pour chaque rendu et est mis à jour en asynchrone. En revanche, lorsque vous modifiez la valeur actuelle d’une ref, c’est immédiat :

ref.current = 5;
console.log(ref.current); // 5

C’est parce que la ref elle-même n’est qu’un objet JavaScript brut, et se comporte donc comme tel.

Vous n’avez pas non plus à vous préoccuper d’éviter les mutations lorsque vous travaillez avec une ref. Du moment que l’objet que vous modifiez n’est pas utilisé pour le rendu, React se fiche de ce que vous faites avec la ref et son contenu.

Les refs et le DOM

Vous pouvez faire pointer votre ref vers ce que vous voulez. Ceci dit, le cas le plus courant pour une ref consiste à accéder à un élément du DOM. C’est par exemple bien pratique pour gérer le focus programmatiquement. Quand vous passez une ref à la prop ref d’un élément en JSX, comme dans <div ref={myRef}>, React référencera l’élément DOM correspondant dans myRef.current. Lorsque l’élément sera retiré du DOM, React recalera ref.current à null. Vous pouvez en apprendre davantage dans Manipuler le DOM avec des refs.

En résumé

  • Les refs sont une échappatoire qui vous permet de conserver des valeurs qui ne servent pas au rendu. Vous n’en aurez pas souvent besoin.
  • Une ref est un objet JavaScript brut avec une unique propriété current, que vous pouvez lire et écrire.
  • Vous pouvez demander à React de vous fournir une ref en appelant le Hook useRef.
  • Tout comme les variables d’état, les refs préservent leur information d’un rendu à l’autre du composant.
  • Contrairement aux états, modifier la valeur current d’une ref ne déclenche pas un nouveau rendu.
  • Évitez de lire ou d’écrire dans ref.current pendant le rendu ; le comportement de votre composant deviendrait imprévisible.

Défi 1 sur 4 ·
Corriger un bouton de discussion

Tapez un message et cliquez sur « Envoyer ». Vous remarquerez un délai de trois secondes avant de voir la notification « Envoyé ! ». Dans l’intervalle, vous pouvez voir un bouton « Annuler ». Ce bouton est supposé empêcher l’apparition de la notification « Envoyé ! ». Pour cela, il appelle clearTimeout sur l’ID de timer sauvegardé lors de handleSend. Pourtant, même après avoir cliqué sur « Annuler », la notification « Envoyé ! » apparaît. Trouvez ce qui cloche et corrigez-le.

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  let timeoutID = null;

  function handleSend() {
    setIsSending(true);
    timeoutID = setTimeout(() => {
      alert('Envoyé !');
      setIsSending(false);
    }, 3000);
  }

  function handleUndo() {
    setIsSending(false);
    clearTimeout(timeoutID);
  }

  return (
    <>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        onClick={handleSend}>
        {isSending ? 'Envoi...' : 'Envoyer'}
      </button>
      {isSending &&
        <button onClick={handleUndo}>
          Annuler
        </button>
      }
    </>
  );
}