Garder les composants purs

Certaines fonctions JavaScript sont pures. Les fonctions pures se contentent de réaliser un calcul, rien de plus. En écrivant rigoureusement vos composants sous forme de fonctions pures, vous éviterez une catégorie entière de bugs ahurissants et de comportements imprévisibles au fil de la croissance de votre base de code. Pour en bénéficier, il vous faut toutefois suivre quelques règles.

Vous allez apprendre

  • Ce qu’est la pureté d’une fonction, et en quoi elle vous aide à éviter les bugs
  • Comment garder vos composants purs en ne modifiant rien pendant la phase de rendu
  • Comment utiliser le Mode Strict pour détecter les erreurs dans vos composants

La pureté : les composants vus comme des formules

En informatique (en particulier dans le monde de la programmation fonctionnelle), une fonction pure a les caractéristiques suivantes :

  • Elle s’occupe de ses affaires. Elle ne modifie aucun objet ou variable qui existaient avant son appel.
  • Pour les mêmes entrées, elle produit la même sortie. Pour un jeu d’entrées données, une fonction pure renverra toujours le même résultat.

Vous avez peut-être déjà l’habitude d’une catégorie de fonctions pures : les formules mathématiques.

Prenons la formule suivante : y = 2x.

Si x = 2 alors y = 4. Toujours.

Si x = 3 alors y = 6. Toujours.

Si x = 3, y ne vaudra pas parfois 9, parfois –1 ou parfois 2,5 en fonction du moment de la journée ou de l’état du marché boursier.

Si y = 2x et x = 3, y vaudra toujours 6.

Si nous en faisions une fonction JavaScript, elle ressemblerait à ça :

function double(number) {
return 2 * number;
}

Dans l’exemple ci-dessus, double est une fonction pure. Si vous lui passez 3, elle renverra 6. Toujours.

React est fondé sur cette notion. React suppose que chaque composant que vous écrivez est une fonction pure. Ça signifie que les composants React que vous écrivez doivent toujours renvoyer le même JSX pour les mêmes entrées :

function Recipe({ drinkers }) {
  return (
    <ol>
      <li>Faire bouillir {drinkers} tasses d’eau.</li>
      <li>Ajouter {drinkers} cuillers de thé et {0.5 * drinkers} cuillers d’épices.</li>
      <li>Ajouter {0.5 * drinkers} tasses de lait jusqu’à ébullition, et du sucre selon les goûts de chacun.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Recette du Chai Épicé</h1>
      <h2>Pour deux</h2>
      <Recipe drinkers={2} />
      <h2>Pour un groupe</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

Lorsque vous passez drinkers={2} à Recipe, il renverra du JSX avec 2 tasses d’eau. Toujours.

Si vous passez drinkers={4}, il renverra du JSX avec 4 tasses d’eau. Toujours.

Comme une formule de maths.

Vous pourriez voir vos composants comme des recettes : si vous les suivez et n’introduisez pas de nouveaux ingrédients lors du processus de confection, vous obtiendrez le même plat à chaque fois. Ce « plat » est le JSX que le composant sert à React pour le rendu.

Une recette de thé pour x personnes : prendre x tasses d’eau, ajouter x cuillers de thé et 0,5x cuillers d’épices, puis 0,5x tasses de lait.

Illustré par Rachel Lee Nabors

Les effets de bord : les conséquences (in)attendues

Le processus de rendu de React doit toujours être pur. Les composants ne devraient renvoyer que leur JSX, et ne pas modifier des objets ou variables qui existaient avant le rendu : ça les rendrait impurs !

Voici un composant qui enfreint cette règle :

let guest = 0;

function Cup() {
  // Erroné : modifie une variable pré-existante !
  guest = guest + 1;
  return <h2>Tasse de thé pour l’invité #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

Ce composant lit et écrit une variable guest déclarée hors de sa fonction. Ça signifie qu’appeler ce composant plusieurs fois produira un JSX différent ! En prime, si d’autres composants lisent guest, eux aussi produiront un JSX différent, selon le moment de leur rendu ! Tout le système devient imprévisible.

Pour en revenir à notre formule y = 2x, désormais même si x = 2, nous n’avons plus la certitude que y = 4. Nos tests pourraient échouer, nos utilisateurs pourraient être désarçonnés, les avions pourraient tomber comme des pierres… Vous voyez bien que ça donnerait des bugs insondables !

Vous pouvez corriger ce composant en passant plutôt guest comme prop :

function Cup({ guest }) {
  return <h2>Tasse de thé pour l’invité #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

Votre composant est désormais pur, et le JSX qu’il renvoie ne dépend que de la prop guest.

De façon générale, vous ne devriez pas exiger un ordre particulier de rendu pour vos composants. Peu importe si vous appelez y = 2x avant ou après y = 5x : les deux formules se calculent indépendamment l’une de l’autre. De la même façon, chaque composant ne devrait que « penser à lui-même » plutôt que de tenter de se coordonner avec d’autres (ou dépendre d’eux) lors du rendu. Le rendu est comme un examen scolaire : chaque composant devrait calculer son JSX par lui-même !

En détail

Détecter les impuretés grâce à StrictMode

Même si vous ne les avez pas encore toutes utilisées à ce stade, sachez que dans React il existe trois types d’entrées que vous pouvez lire lors d’un rendu : les props, l’état, et le contexte. Vous devriez toujours considérer ces trois entités comme étant en lecture seule.

Lorsque vous souhaitez modifier quelque chose en réaction à une interaction utilisateur, vous devriez mettre à jour l’état plutôt que d’écrire dans une variable. Vous ne devriez jamais modifier des variables ou objets pré-existants lors du rendu de votre composant.

React propose un « Mode Strict » dans lequel il appelle chaque fonction composant deux fois pendant le développement. En appelant chaque fonction composant deux fois, le Mode Strict vous aide à repérer les composants qui enfreignent ces règles.

Avez-vous remarqué que le premier exemple affichait « invité #2 », « invité #4 » et « invité #6 » au lieu de « invité #1 », « invité #2 » et « invité #3 » ? La fonction d’origine était impure, de sorte que l’appeler deux fois cassait son fonctionnement. Mais la fonction corrigée, qui est pure, fonctionne même si elle est systématiquement appelée deux fois. Les fonctions pures font juste un calcul, aussi les appeler deux fois ne change rien, tout comme appeler double(2) deux fois ne change pas son résultat, et résoudre y = 2x deux fois ne change pas la valeur de y. Mêmes entrées, même sorties. Toujours.

Le Mode Strict n’a aucun effet en production, il ne ralentira donc pas votre appli pour vos utilisateurs. Pour activer le Mode Strict, enrobez votre composant racine dans un <React.StrictMode>. Certains frameworks mettent ça en place par défaut.

Les mutations locales : les petits secrets de votre composant

Dans l’exemple ci-avant, le problème venait de ce que le composant modifiait une variable pré-existante pendant son rendu. On parle alors souvent de « mutation » pour rendre ça un peu plus effrayant. Les fonctions pures ne modifient pas les variables hors de leur portée, ni les objets créés avant l’appel : si elles le faisaient, ça les rendrait impures !

En revanche, il est parfaitement acceptable de modifier les variables ou objets que vous venez de créer pendant le rendu. Dans l’exemple qui suit, vous créez un tableau [], l’affectez à une variable cups puis y ajoutez une douzaine de tasses à coups de push :

function Cup({ guest }) {
  return <h2>Tasse de thé pour l’invité #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

Si la variable cups ou le tableau [] avaient été créés hors de la fonction TeaGathering, ça aurait posé un énorme problème ! Vous auriez modifié un objet pré-existant en ajoutant des éléments au tableau.

Mais là, tout va bien parce que vous les avez créés pendant ce même rendu, au sein de TeaGathering. Aucun code hors de TeaGathering ne saura jamais que ça s’est passé. On parle alors de mutation locale ; c’est comme un petit secret de votre composant.

Les endroits où vous pouvez causer des effets de bord

S’il est vrai que la programmation fonctionnelle s’appuie fortement sur la pureté, à un moment, quelque part, quelque chose va devoir changer. C’est un peu l’objectif d’un programme ! Ces modifications (mettre à jour l’affichage, démarrer une animation, modifier des données) sont appelées des effets de bord. C’est ce qui arrive « à côté », et non au sein du rendu.

Dans React, les effets de bord résident généralement dans des gestionnaires d’événements. Les gestionnaires d’événements sont des fonctions que React exécute lorsque vous faites une action donnée — par exemple lorsque vous cliquez sur un bouton. Même si les gestionnaires d’événements sont définis au sein de votre composant, il ne sont pas exécutés pendant le rendu ! Du coup, les gestionnaires d’événements n’ont pas besoin d’être purs.

Si vous avez épuisé toutes les autres options et ne pouvez pas trouver un gestionnaire d’événement adéquat pour votre effet de bord, vous pouvez tout de même en associer un au JSX que vous renvoyez en appelant useEffect dans votre composant. Ça dit à React de l’exécuter plus tard, après le rendu, lorsque les effets de bord seront autorisés. Ceci dit, cette approche doit être votre dernier recours.

Lorsque c’est possible, essayez d’exprimer votre logique rien qu’avec le rendu. Vous serez surpris·e de tout ce que ça permet de faire !

En détail

Pourquoi React tient-il tant à la pureté ?

Écrire des fonctions pures nécessite de la pratique et de la discipline. Mais ça ouvre aussi des opportunités merveilleuses :

  • Vos composants peuvent être exécutés dans différents environnements — par exemple sur le serveur ! Puisqu’il renvoie toujours le même résultat pour les mêmes entrées, un composant peut servir à de nombreuses requêtes utilisateur.
  • Vous pouvez améliorer les performances en sautant le rendu des composants dont les entrées n’ont pas changé. C’est sans risque parce que les fonctions pures renvoient toujours les mêmes résultats, on peut donc les mettre en cache.
  • Si certaines données changent au cours du rendu d’une arborescence profonde de composants, React peut redémarrer le rendu sans perdre son temps à finir celui en cours, désormais obsolète. La pureté permet de stopper le calcul à tout moment, sans risque.

Toutes les nouvelles fonctionnalités de React que nous sommes en train de construire tirent parti de la pureté. Du chargement de données aux performances en passant par les animations, garder vos composants purs permet d’exploiter la pleine puissance du paradigme de React.

En résumé

  • Un composant doit être pur, ce qui signifie que :
    • Il s’occupe de ses affaires. Il ne modifie aucun objet ou variable qui existaient avant son rendu.
    • Pour les mêmes entrées, il produit la même sortie. Pour un jeu d’entrées données, un composant renverra toujours le même JSX.
  • Le rendu peut survenir à tout moment, c’est pourquoi les composants ne doivent pas dépendre de leurs positions respectives dans la séquence de rendu.
  • Vous ne devriez pas modifier les entrées utilisées par vos composants pour leur rendu. Ça concerne les props, l’état et le contexte. Pour mettre à jour l’affichage, mettez à jour l’état plutôt que de modifier des objets pré-existants.
  • Faites le maximum pour exprimer la logique de votre composant dans le JSX que vous renvoyez. Lorsque vous devez absolument « modifier un truc », vous voudrez généralement le faire au sein d’un gestionnaire d’événement. En dernier recours, vous pouvez utiliser useEffect.
  • Écrire des fonctions pures nécessite un peu de pratique, mais ça permet d’exploiter la pleine puissance du paradigme de React.

Défi 1 sur 3 ·
Réparer une horloge

Ce composant essaie de passer la classe CSS du <h1> à "night" entre minuit et six heures du matin, et à "day" le reste de la journée. Cependant, il ne fonctionne pas. Pouvez-vous le corriger ?

Vous pouvez vérifier si votre solution fonctionne en modifiant temporairement le fuseau horaire de votre ordinateur. Lorsque l’horloge est entre minuit et six heures du matin, elle devrait être en couleurs inversées !

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}