Transfert de refs

Le transfert de ref est une technique permettant de déléguer automatiquement une ref d’un composant à l’un de ses enfants. Ça n’est généralement pas nécessaire pour la plupart des composants dans une application. Cependant, ça peut être utile pour certains types de composants, particulièrement dans les bibliothèques de composants réutilisables. Les scénarios les plus fréquents sont décrits ci-dessous.

Transfert de refs vers des composants du DOM

Prenons un composant FancyButton qui affiche l’élément DOM natif button :

function FancyButton(props) {
  return (
    <button className="FancyButton">
      {props.children}
    </button>
  );
}

Les composants React masquent leurs détails d’implémentation, y compris leur rendu. Les autres composants utilisant FancyButton n’auront généralement pas besoin d’obtenir une ref sur l’élément DOM interne button. C’est une bonne chose, car ça empêche les composants de trop s’appuyer sur la structure DOM les uns et des autres.

Bien qu’une telle encapsulation soit souhaitable pour les composants applicatifs tels que FeedStory ou Comment, elle peut être gênante pour les composants hautement réutilisables, tels que FancyButton ou MyTextInput. Ces composants ont tendance à être utilisés un peu partout dans dans l’application de manière similaire à un button ou un input, et l’accès à leurs nœuds DOM peut s’avérer nécessaire pour la gestion du focus, de la sélection ou des animations.

Le transfert de ref est une fonctionnalité optionnelle qui permet à certains composants de prendre une ref qu’ils reçoivent et de la passer plus bas dans l’arbre (en d’autres termes, la « transférer ») à un composant enfant.

Dans l’exemple ci-dessous, FancyButton utilise React.forwardRef pour obtenir la ref qui lui est passée, puis la transfère au button DOM qu’il affiche :

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// Vous pouvez maintenant obtenir une ref directement attachée au bouton DOM :
const ref = React.createRef();
<FancyButton ref={ref}>Cliquez ici</FancyButton>;

De cette façon, les composants utilisant FancyButton peuvent obtenir une ref sur le nœud DOM button sous-jacent et y accéder si nécessaire, comme s’ils utilisaient directement un button DOM.

Voici une explication étape par étape de ce qui se passe dans l’exemple ci-dessus :

  1. Nous créons une ref React en appelant React.createRef et l’affectons à une variable ref.
  2. Nous passons notre ref à <FancyButton ref={ref}> en la spécifiant comme un attribut JSX.
  3. React transmet la ref à la fonction (props, ref) => ... à l’intérieur de forwardRef comme deuxième argument.
  4. Nous transférons cet argument ref au <button ref={ref}> en le spécifiant comme un attribut JSX.
  5. Quand la ref est liée, ref.current pointera vers le nœud DOM button.

Remarque

Le second argument ref n’existe que quand vous définissez un composant avec l’appel à React.forwardRef. Les fonctions composants habituelles et les composants à base de classes ne reçoivent pas l’argument ref, et la ref n’est pas non plus disponible dans les props du composant.

Le transfert de refs n’est pas limité aux composants DOM. Vous pouvez aussi transférer des refs vers des instances de classe de composant.

Note pour les mainteneurs de bibliothèques de composants

Lorsque vous commencez à utiliser forwardRef dans une bibliothèque de composants, vous devez le traiter comme une rupture de compatibilité ascendante et publier une nouvelle version majeure de votre bibliothèque. En effet, votre bibliothèque a probablement un comportement différent (par exemple la cible d’affectation des refs et la nature des types exportés), et ça pourrait casser les applications et autres bibliothèques qui dépendent de l’ancien comportement.

L’application conditionnelle de React.forwardRef lorsqu’elle existe est également déconseillée pour les mêmes raisons : ça modifie le comportement de votre bibliothèque et pourrait casser les applications de vos utilisateurs lorsqu’ils mettent à jour React.

Transfert des refs dans les composants d’ordre supérieur

Cette technique peut aussi être particulièrement utile avec les composants d’ordre supérieur (Higher-Order Components ou HOC, NdT). Commençons par un exemple de HOC qui journalise les props du composant dans la console :

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Anciennes props :', prevProps);
      console.log('Nouvelles props :', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

Le HOC logProps transmet toutes les props au composant qu’il enrobe, ainsi le résultat affiché sera la même. Par exemple, nous pouvons utiliser ce HOC pour lister toutes les props transmises à notre composant fancy button :

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Plutôt que d'exporter FancyButton, nous exportons LogProps.
// Cependant, ça affichera tout de même un FancyButton.
export default logProps(FancyButton);

Il y a une limitation dans l’exemple ci-dessus : les refs ne seront pas transférées. C’est parce que ref n’est pas une prop. Comme key, elle est gérée différemment par React. Si vous ajoutez une ref à un HOC, la ref fera référence au composant conteneur extérieur, et non au composant enrobé.

Ça signifie que les refs destinées à notre composant FancyButton seront en réalité attachées au composant LogProps :

import FancyButton from './FancyButton';

const ref = React.createRef();

// Le composant FancyButton que nous avons importé est le HOC LogProps.  Même si
// l’affichage sera le même, notre ref visera LogProps au lieu du composant
// FancyButton !  Ça signifie par exemple que nous ne pouvons pas appeler
<FancyButton
  label="Cliquez ici"
  handleClick={handleClick}
  ref={ref}
/>;

Heureusement, nous pouvons explicitement transférer les refs au composant FancyButton interne à l’aide de l’API React.forwardRef. Celle-ci accepte une fonction de rendu qui reçoit les arguments props et ref et renvoie un nœud React. Par exemple :

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Anciennes props :', prevProps);
      console.log('Nouvelles props :', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // Affecte la prop personnalisée "forwardedRef" en tant que ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // Remarquez le deuxième paramètre `ref` fourni par `React.forwardRef`.  Nous
  // pouvons le passer à LogProps comme une prop normale, par exemple
  // `forwardedRef`. Et il peut ensuite être attaché au composant.
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

Affichage d’un nom personnalisé dans les DevTools

React.forwardRef accepte une fonction de rendu. Les outils de développement React (React DevTools, NdT) utilisent cette fonction pour déterminer quoi afficher pour le composant de transfert de ref.

Par exemple, le composant suivant apparaîtra sous le nom ”ForwardRef” dans les DevTools :

const WrappedComponent = React.forwardRef((props, ref) => {
  return <LogProps {...props} forwardedRef={ref} />;
});

Si vous nommez la fonction de rendu, les DevTools incluront également son nom (par exemple, ”ForwardRef(myFunction)”) :

const WrappedComponent = React.forwardRef(
  function myFunction(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }
);

Vous pouvez même définir la propriété displayName de la fonction pour y inclure le composant que vous enrobez :

function logProps(Component) {
  class LogProps extends React.Component {
    // ...
  }

  function forwardRef(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }

  // Donne à ce composant un nom d’affichage plus utile dans les DevTools.
  // exemple : "ForwardRef(logProps(MyComponent))"
  const name = Component.displayName || Component.name;
  forwardRef.displayName = `logProps(${name})`;

  return React.forwardRef(forwardRef);
}