Préserver et réinitialiser l'état

L’état est isolé entre les composants. React garde en mémoire quel état appartient à quel composant en fonction de leur place dans l’arbre de l’interface utilisateur (l’UI). Vous pouvez contrôler quand préserver l’état et quand le réinitialiser entre les différents rendus.

Vous allez apprendre

  • Comment React « voit » les structures des composants.
  • Quand React choisit de préserver ou de réinitialiser l’état.
  • Comment forcer React à réinitialiser l’état d’un composant.
  • Comment les clés et les types affectent la préservation de l’état.

L’arbre de l’UI

Les navigateurs utilisent différentes structures arborescentes pour modéliser l’UI. Le DOM représente les éléments HTML, le CSSOM fait la même chose pour le CSS. Il existe même un arbre d’accessibilité !

React utilise également des structures arborescentes pour gérer et modéliser l’UI que vous faites. React crée des arbres d’UI à partir de votre JSX. Ensuite, React DOM met à jour les éléments du DOM du navigateur pour qu’ils correspondent à cet arbre de l’UI (React Native retranscrit ces arbres en composants spécifiques aux plateformes mobiles).

Diagramme avec trois sections réparties horizontalement. Dans la première section, il y a trois rectangles empilés verticalement, appelés 'Component A', 'Component B' et 'Component C'. La transition vers le volet suivant est faite par une flèche avec le logo React au-dessus et appelée 'React'. La section du milieu contient un arbre de composants dont la racine est appelée 'A', avec deux enfants 'B' et 'C'. La section suivante est à nouveau transposée en utilisant une flèche avec le logo 'React' au dessus. La troisième et dernière section est une représentation schématisée d'un navigateur contenant un arbre de 8 nœuds, dont seul un sous-ensemble est surligné (indiquant le sous-arbre de la section du milieu).
Diagramme avec trois sections réparties horizontalement. Dans la première section, il y a trois rectangles empilés verticalement, appelés 'Component A', 'Component B' et 'Component C'. La transition vers le volet suivant est faite par une flèche avec le logo React au-dessus et appelée 'React'. La section du milieu contient un arbre de composants dont la racine est appelée 'A', avec deux enfants 'B' et 'C'. La section suivante est à nouveau transposée en utilisant une flèche avec le logo 'React' au dessus. La troisième et dernière section est une représentation schématisée d'un navigateur contenant un arbre de 8 nœuds, dont seul un sous-ensemble est surligné (indiquant le sous-arbre de la section du milieu).

À partir des composants, React créé un arbre d’UI que React DOM utilise pour faire le rendu du DOM.

L’état est lié à une position dans l’arbre

Lorsque vous donnez un état à un composant, vous pouvez penser que l’état « vit » à l’intérieur du composant. En réalité, l’état est conservé à l’intérieur de React. React associe chaque élément d’état qu’il conserve au composant correspondant en fonction de la place que celui-ci occupe dans l’arbre de l’UI.

Ci-dessous, il n’y a qu’une seule balise <Counter />, pourtant elle est affichée à deux positions différentes :

import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Voici comment les visualiser dans un arbre :

Diagramme d'un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. Chacun d'eux est appelé 'Counter' et contient une bulle d'état appelée 'count' dont la valeur est à 0.
Diagramme d'un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. Chacun d'eux est appelé 'Counter' et contient une bulle d'état appelée 'count' dont la valeur est à 0.

L’arbre de React

Il s’agit de deux compteurs distincts car chacun d’eux est rendu sur sa propre position dans l’arbre. Généralement, vous n’avez pas besoin de penser à ces positions pour utiliser React, mais il peut être utile de savoir comment ça fonctionne.

Dans React, chaque composant à l’écran a son propre état complétement isolé. Par exemple, si vous faites le rendu de deux composants Counter l’un à côté de l’autre, chacun d’eux aura ses propres variables d’état indépendantes de score et d’hover.

Cliquez sur chaque compteur et voyez comment ils n’affectent pas l’autre :

import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Comme vous pouvez le voir, quand un compteur est mis à jour, seul l’état de ce composant est mis à jour :

Diagramme avec un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. L'enfant à gauche est appelé 'Counter' et contient un bulle d'état appelée 'count' ayant une valeur à 0. L'enfant à droite est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 1. La bulle d'état de l'enfant à droite est surlignée en jaune afin d'indiquer que sa valeur a été mise à jour.
Diagramme avec un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. L'enfant à gauche est appelé 'Counter' et contient un bulle d'état appelée 'count' ayant une valeur à 0. L'enfant à droite est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 1. La bulle d'état de l'enfant à droite est surlignée en jaune afin d'indiquer que sa valeur a été mise à jour.

Mise à jour de l’état

React conservera l’état tant que vous ferez le rendu du même composant à la même position. Pour le voir, incrémentez les deux compteurs, puis supprimez le deuxième composant en décochant « Afficher le deuxième compteur », en enfin remettez-le en cochant à nouveau la case :

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        Afficher le deuxième compteur
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Remarquez qu’au moment où vous arrêtez le rendu du deuxième compteur son état disparaît complétement. Lorsque React supprime un composant, il supprime également son état.

Diagramme avec un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. L'enfant à gauche est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0. L'enfant à droite est manquant, et à sa place est affichée une image avec des étincelles indiquant qu'il a été supprimé de l'arbre.
Diagramme avec un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. L'enfant à gauche est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0. L'enfant à droite est manquant, et à sa place est affichée une image avec des étincelles indiquant qu'il a été supprimé de l'arbre.

Supprimer un composant

Lorsque vous cochez « Afficher le deuxième compteur », un deuxième Counter avec son état associé sont initialisés de zéro (score = 0), puis ajoutés au DOM.

Diagramme d'un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. L'enfant à gauche est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0. L'enfant à droite est appelé 'Counter' et contient une bulle d'état 'count' valant 0. Tout le nœud de l'enfant à droite est surligné en jaune, indiquant qu'il vient juste d'être ajouté à l'arbre.
Diagramme d'un arbre de composants React. Le nœud racine est appelé 'div' et a deux enfants. L'enfant à gauche est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0. L'enfant à droite est appelé 'Counter' et contient une bulle d'état 'count' valant 0. Tout le nœud de l'enfant à droite est surligné en jaune, indiquant qu'il vient juste d'être ajouté à l'arbre.

Ajout d’un composant

React préserve l’état d’un composant tant qu’il est affiché sur sa position dans l’arbre de l’UI. S’il est supprimé, ou si un composant différent est affiché sur la même position, alors React se débarrasse de son état.

Le même composant à la même position préserve son état

Dans cet exemple, il y a deux balises différentes <Counter /> :

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Utiliser un style fantaisiste
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Quand vous cocher ou décochez la case, l’état du compteur n’est pas réinitialisé. Que isFancy soit à true ou à false, vous avez toujours un <Counter /> comme premier enfant du div renvoyé par le composant racine App :

Diagramme avec deux sections séparées par une flèche allant de l'une à l'autre. Chaque section affiche une structure de composants avec un parent appelé 'App', contenant une bulle d'état appelée 'isFancy'. Ce composant a un enfant appelé 'div', qui amène à une bulle de prop contenent 'isFancy' (qui est affichée en violet) qui est donnée plus bas à l'enfant unique. Le dernier enfant est appelé 'Counter' et contient une bulle d'état appelée 'count' dont la valeur est à 3 dans les deux diagrammes. Dans la section de gauche du diagramme, il n'y a rien de surligné et la valeur de l'état 'isFancy' du parent est à false. Dans la section de droite, la valeur de l'état 'isFancy' a été changée à true, et est surlignée en jaune, de la même façon que la bulle plus bas, qui a aussi sa valeur 'isFancy' à true.
Diagramme avec deux sections séparées par une flèche allant de l'une à l'autre. Chaque section affiche une structure de composants avec un parent appelé 'App', contenant une bulle d'état appelée 'isFancy'. Ce composant a un enfant appelé 'div', qui amène à une bulle de prop contenent 'isFancy' (qui est affichée en violet) qui est donnée plus bas à l'enfant unique. Le dernier enfant est appelé 'Counter' et contient une bulle d'état appelée 'count' dont la valeur est à 3 dans les deux diagrammes. Dans la section de gauche du diagramme, il n'y a rien de surligné et la valeur de l'état 'isFancy' du parent est à false. Dans la section de droite, la valeur de l'état 'isFancy' a été changée à true, et est surlignée en jaune, de la même façon que la bulle plus bas, qui a aussi sa valeur 'isFancy' à true.

Mettre à jour l’état de App ne remet pas à jour le Counter parce que ce dernier reste sur la même position

C’est le même composant sur la même position, donc du point de vue de React, il s’agit du même compteur.

Piège

Souvenez-vous qu’il s’agit de la position dans l’arbre de l’UI — et non dans le JSX — qui importe à React ! Ce composant a deux clauses return avec des balises JSX différentes de <Counter /> à l’intérieur et l’extérieur du if :

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Utiliser un style fantaisiste
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Utiliser un style fantaisiste
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Vous pourriez supposer que l’état se mette à jour quand vous cochez la case, mais ce n’est pas le cas ! Ça s’explique par le fait que les deux balises <Counter /> sont affichées à la même position. React ne sait pas où vous placez les conditions dans votre fonction. Tout ce qu’il « voit » c’est l’arbre qui est renvoyé.

Dans les deux cas, le composant App renvoie un <div> avec un <Counter /> comme premier enfant. Pour React, ces deux compteurs ont la même « adresse » : le premier enfant du premier enfant de la racine. C’est ainsi que React les associe entre le rendu précédent et le suivant, peu importe la façon dont vous structurez votre logique.

Des composants différents à la même position réinitialisent l’état

Dans cet exemple, cliquez sur la case remplacera <Counter> par un <p>:

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>À bientôt !</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Faire une pause
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Vous échangez ici deux types de composants différents sur la même position. À l’origine, le premier enfant du <div> contenait un Counter. Ensuite, comme vous l’avez échangé avec un p, React a supprimé le Counter de l’UI et détruit son état.

Diagramme avec trois sections, avec une flèche allant d'une section à une autre. La première section contient un composant React appelé 'div' avec un seul enfant 'Counter' contenant une bulle d'état appelée 'count', avec une valeur à 3. La section du milieu a le même parent 'div', mais le composant enfant a maintenant été supprimé, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'p', surligné en jaune.
Diagramme avec trois sections, avec une flèche allant d'une section à une autre. La première section contient un composant React appelé 'div' avec un seul enfant 'Counter' contenant une bulle d'état appelée 'count', avec une valeur à 3. La section du milieu a le même parent 'div', mais le composant enfant a maintenant été supprimé, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'p', surligné en jaune.

Quand Counter est changé en p, le Counter est supprimé et le p est ajouté

Diagramme avec trois sections, avec une flèche allant d'une section à une autre. La première section contient un composant React appelé 'p'. La section du milieu a le même parent 'div', mais le composant enfant a maintenant été supprimé, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'Counter' contenant une bulle d'état 'count' de valeur 0, surligné en jaune.
Diagramme avec trois sections, avec une flèche allant d'une section à une autre. La première section contient un composant React appelé 'p'. La section du milieu a le même parent 'div', mais le composant enfant a maintenant été supprimé, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'Counter' contenant une bulle d'état 'count' de valeur 0, surligné en jaune.

En revenant en arrière, le p est supprimé et le Counter est ajouté

Ainsi, quand vous faites le rendu d’un composant différent à la même position, l’état de tout son sous-arbre est réinitialisé. Pour comprendre comment ça fonctionne, incrémentez le compteur et cochez la case :

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Utiliser un style fantaisiste
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

L’état du compteur se réinitialise quand vous cliquez sur la case. Bien que vous fassiez le rendu d’un Counter, le premier enfant du div passe d’un div à une section. Lorsque l’enfant div a été retiré du DOM, tout l’arbre en dessous de lui (ce qui inclut le Counter et son état) a également été détruit.

Diagramme avec trois sections, avec une flèche allant d'une section à l'autre. La première section contient un composant React appelé 'div' avec un seul enfant appelé 'section', qui lui-même n'a qu'un seul enfant appelé 'Counter', qui dispose d'une bulle d'état appelé 'count' dont la valeur est à 3. La section du milieu a le même parent 'div', mais les composants enfants ont maintenant été supprimés, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'div' surligné en jaune, ainsi qu'un nouvel enfant appelé 'Counter' contenant une bulle d'état appelée 'count' avec une valeur à 0, le tout surligné en jaune.
Diagramme avec trois sections, avec une flèche allant d'une section à l'autre. La première section contient un composant React appelé 'div' avec un seul enfant appelé 'section', qui lui-même n'a qu'un seul enfant appelé 'Counter', qui dispose d'une bulle d'état appelé 'count' dont la valeur est à 3. La section du milieu a le même parent 'div', mais les composants enfants ont maintenant été supprimés, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'div' surligné en jaune, ainsi qu'un nouvel enfant appelé 'Counter' contenant une bulle d'état appelée 'count' avec une valeur à 0, le tout surligné en jaune.

Quand la section change pour un div, la section est supprimée est le nouveau div est ajouté

Diagramme avec trois sections, avec une flèche allant d'une section à l'autre. La première section contient un composant React appelé 'div' avec un seul enfant appelé 'div', qui lui-même n'a qu'un seul enfant appelé 'Counter', qui dispose d'une bulle d'état appelé 'count' dont la valeur est à 0. La section du milieu a le même parent 'div', mais les composants enfants ont maintenant été supprimés, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'section' surligné en jaune, ainsi qu'un nouvel enfant appelé 'Counter' contenant une bulle d'état appelée 'count' de valeur 0, le tout surligné en jaune.
Diagramme avec trois sections, avec une flèche allant d'une section à l'autre. La première section contient un composant React appelé 'div' avec un seul enfant appelé 'div', qui lui-même n'a qu'un seul enfant appelé 'Counter', qui dispose d'une bulle d'état appelé 'count' dont la valeur est à 0. La section du milieu a le même parent 'div', mais les composants enfants ont maintenant été supprimés, indiqué par une image avec des étincelles. La troisième section a toujours le même parent 'div', mais avec un nouvel enfant appelé 'section' surligné en jaune, ainsi qu'un nouvel enfant appelé 'Counter' contenant une bulle d'état appelée 'count' de valeur 0, le tout surligné en jaune.

En revenant en arrière, le div est supprimé et la nouvelle section est ajoutée

De manière générale, si vous voulez préserver l’état entre les rendus, la structure de votre arbre doit « se ressembler » d’un rendu à l’autre. Si la structure est différente, l’état sera détruit car React détruit l’état quand il enlève un composant de l’arbre.

Piège

Voici pourquoi il ne faut pas imbriquer les définitions des fonctions des composants.

Ici, la fonction du composant MyTextField est définie à l’intérieur de MyComponent :

import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Cliqué {counter} fois</button>
    </>
  );
}

Chaque fois que vous appuyez sur le bouton, l’état du champ de saisie disparaît ! C’est parce qu’une fonction MyTextField différente est créé à chaque rendu de MyComponent. Comme vous faites le rendu d’un composant différent à la même position, React réinitialise tout l’état en-dessous. Ça amène des bugs et des problèmes de performances. Pour éviter ce problème, déclarez toujours les fonctions de composants au niveau le plus haut, et n’imbriquez pas leurs définitions.

Réinitialiser l’état sur la même position

Par défaut, React préserve l’état d’un composant tant que celui-ci conserve sa position. Généralement, c’est exactement ce que vous voulez, c’est donc logique qu’il s’agisse du comportement par défaut. Cependant, il peut arriver que vous vouliez réinitialiser l’état d’un composant. Regardez cette appli qui permet à deux joueurs de conserver leur score pendant leur tour :

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Clara" />
      ) : (
        <Counter person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Joueur suivant !
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>Score de {person} : {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Pour le moment, le score est conservé quand vous changez de joueur. Les deux Counter apparaissent à la même position, donc React les voit comme le même Counter dont la prop person a changé.

Conceptuellement, dans cette appli, ils doivent être considérés comme deux compteurs distincts. Ils doivent apparaître à la même place dans l’UI, mais l’un est pour Clara, l’autre pour Sarah.

Il y a deux façons de réinitialiser l’état lorsqu’on passe de l’un à l’autre :

  1. Faire le rendu des composants dans deux positions différentes.
  2. Donner explicitement à chaque composant une identité avec key.

Option 1 : faire le rendu d’un composant à différentes positions

Si vous souhaitez rendre ces deux Counter indépendants, vous pouvez faire leur rendu dans deux positions différentes :

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Clara" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Joueur suivant !
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>Score de {person} : {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

  • Initialement, isPlayerA vaut true. Ainsi la première position contient l’état de Counter et la seconde est vide.
  • Quand vous cliquez sur le bouton « Joueur suivant », la première position se vide et la seconde contient désormais un Counter.
Diagramme d'un arbre de composants React. Le parent est appelé 'Scoreboard' avec une bulle d'état appelée isPlayerA qui vaut 'true'. Le seul enfant, placé à gauche, est appelé Counter, avec une bulle d'état appelée 'count' dont la valeur est à 0. L'enfant à gauche est entièrement surligné en jaune, indiquant qu'il a été ajouté.
Diagramme d'un arbre de composants React. Le parent est appelé 'Scoreboard' avec une bulle d'état appelée isPlayerA qui vaut 'true'. Le seul enfant, placé à gauche, est appelé Counter, avec une bulle d'état appelée 'count' dont la valeur est à 0. L'enfant à gauche est entièrement surligné en jaune, indiquant qu'il a été ajouté.

État initial

Diagramme d'un arbre de composants React. Le parent est appelé 'Scoreboard' avec une bulle d'état appelée isPlayerA qui vaut 'false'. La bulle d'état est surlignée en jaune, indiquant qu'elle a changé. L'enfant à gauche est remplacé par une image avec des étincelles, indiquant qu'il a été supprimé, et il y a désormais un nouvel enfant à droite, surligné en jaune indiquant qu'il a été ajouté. Le nouvel enfant est appelée 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0.
Diagramme d'un arbre de composants React. Le parent est appelé 'Scoreboard' avec une bulle d'état appelée isPlayerA qui vaut 'false'. La bulle d'état est surlignée en jaune, indiquant qu'elle a changé. L'enfant à gauche est remplacé par une image avec des étincelles, indiquant qu'il a été supprimé, et il y a désormais un nouvel enfant à droite, surligné en jaune indiquant qu'il a été ajouté. Le nouvel enfant est appelée 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0.

Appuyer sur « Joueur suivant »

 Un diagramme d'un arbre de composants React. Le parent est appelé 'Scoreboard' avec une bulle d'état appelée isPlayerA qui vaut 'true'. La bulle d'état est surlignée en jaune, indiquant qu'elle a changé. Il y a un nouvel enfant à gauche, surligné en jaune pour indiquer qu'il a été ajouté. Ce nouvel enfant est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0. L'enfant à droite est remplacé par une image avec des étincelles, indiquant qu'il a été supprimé.
 Un diagramme d'un arbre de composants React. Le parent est appelé 'Scoreboard' avec une bulle d'état appelée isPlayerA qui vaut 'true'. La bulle d'état est surlignée en jaune, indiquant qu'elle a changé. Il y a un nouvel enfant à gauche, surligné en jaune pour indiquer qu'il a été ajouté. Ce nouvel enfant est appelé 'Counter' et contient une bulle d'état appelée 'count' avec une valeur à 0. L'enfant à droite est remplacé par une image avec des étincelles, indiquant qu'il a été supprimé.

Appuyer à nouveau sur « Joueur suivant »

Chaque état de Counter est supprimé dès que ce dernier est retiré du DOM. C’est pour ça qu’il est réinitialisé à chaque fois que vous appuyez sur le bouton.

Cette solution est pratique quand vous n’avez qu’un petit nombre de composants indépendants à afficher à la même place. Dans cet exemple, vous n’en avez que deux, ce n’est donc pas compliqué de faire leurs rendus séparément dans le JSX.

Option 2 : réinitialiser l’état avec une clé

Il existe une méthode plus générique pour réinitialiser l’état d’un composant.

Vous avez peut-être déjà vu les key lors du rendu des listes. Ces clés ne sont pas réservées aux listes ! Vous pouvez les utiliser pour aider React à faire la distinction entre les composants. Par défaut, React utilise l’ordre dans un parent (« premier compteur », « deuxième compteur ») pour différencier les composants. Toutefois, les clés vous permettent de dire à React qu’il ne s’agit pas simplement d’un premier compteur ou d’un deuxième compteur, mais plutôt un compteur spécifique — par exemple le compteur de Clara. De cette façon, React reconnaitra le compteur de Clara où qu’il apparaisse dans l’arbre.

Dans cet exemple, les deux <Counter /> ne partagent pas leur état, bien qu’ils apparaissent à la même place dans le JSX :

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Clara" person="Clara" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Joueur suivant !
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>Score de {person} : {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Incrémenter
      </button>
    </div>
  );
}

Remplacer Clara par Sarah ne préverse pas l’état. C’est parce que vous leur avez donné des key différentes :

{isPlayerA ? (
<Counter key="Clara" person="Clara" />
) : (
<Counter key="Sarah" person="Sarah" />
)}

Le fait de spécifier une key indique à React de l’utiliser également comme élément de position, plutôt que son ordre au sein du parent. Ainsi, même si vous faites le rendu à la même position dans le JSX, React les voit comme deux compteurs distincts qui ne partageront jamais leur état. À chaque fois qu’un compteur apparaît à l’écran, son état est créé. À chaque fois qu’il est supprimé, son état est supprimé. Passer de l’un à l’autre réinitialise leur état, et ainsi de suite.

Remarque

Retenez que les clés ne sont pas uniques au niveau global. Elles spécifient uniquement la position au sein du parent.

Réinitialiser un formulaire avec une clé

Réinitialiser un état avec une clé est particulièrement utile quand on manipule des formulaires.

Dans cette appli de discussions, le composant <Chat> contient l’état du champ de saisie :

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Clara', email: 'clara@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Essayez de saisir quelque chose dans le champ, puis appuyez sur « Alice » ou « Bob » pour choisir un destinataire différent. Vous noterez que le champ de saisie est conservé parce que le <Chat> est rendu à la même position dans l’arbre.

Dans beaucoup d’applis, c’est le comportement désiré, mais pas dans cette appli de discussion ! Vous ne souhaitez pas qu’un utilisateur envoie un message qu’il a déjà tapé à la mauvaise personne à la suite d’un clic malencontreux. Pour corriger ça, ajoutez une key :

<Chat key={to.id} contact={to} />

Ça garantit que lorsque vous sélectionnez un destinataire différent, le composant Chat sera recréé de zéro, ce qui inclut tout l’état dans l’arbre en dessous. React recréera également tous les éléments DOM plutôt que de les réutiliser.

Désormais, changer de destinataire vide le champ de saisie :

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.id} contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Clara', email: 'clara@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

En détail

Préserver l’état des composants supprimés

Dans une véritable appli de discussion, vous souhaiterez probablement récupérer l’état de la saisie lorsque l’utilisateur resélectionne le destinataire précédent. Il existe plusieurs manières de garder « vivant » l’état d’un composant qui n’est plus visible :

  • Vous pouvez afficher tous les chats plutôt que le seul chat actif, mais en masquant les autres avec du CSS. Les chats ne seraient pas supprimés de l’arbre, de sorte que leur état local serait préservé. Cette solution fonctionne très bien pour des UI simples. Cependant, ça peut devenir très lent si les arbres cachés sont grands et contiennent de nombreux nœuds DOM.
  • Vous pouvez faire remonter l’état et conserver dans le composant parent le message en attente pour chaque destinataire. De cette façon, le fait que les composants enfants soient supprimés importe peu car c’est le parent qui conserve les informations importantes. C’est la solution la plus courante.
  • Vous pouvez aussi utiliser une source différente en plus de l’état React. Par exemple, vous souhaitez sans doute qu’un brouillon du message persiste même si l’utilisateur ferme accidentellement la page. Pour implémenter ça, vous pouvez faire en sorte que le composant Chat intialise son état en lisant le localStorage et y sauve également les brouillons.

Quelle que soit votre stratégie, une discussion avec Alice est conceptuellement différente d’une autre avec Bob, il est donc naturel de donner une key à l’arbre <Chat> en fonction du destinataire actuel.

En résumé

  • React conserve l’état tant que le même composant est rendu à la même position.
  • L’état n’est pas conservé dans les balises JSX. Il est associé à la position dans l’arbre où vous placez ce JSX.
  • Vous pouvez forcer un sous-arbre à réinitialiser son état en donnant une clé différente.
  • N’imbriquez pas les définitions de composants ou vous allez accidentellement réinitialiser leur état.

Défi 1 sur 5 ·
Corriger une saisie qui disparait

Cet exemple montre un message quand vous appuyez sur le bouton. Cependant, appuyer sur ce bouton vide aussi le champ de saisie. Pourquoi ça arrive ? Corrigez ça pour que le champ de saisie ne se vide pas quand on appuie sur le bouton.

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Indice : votre ville préférée ?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Cacher l'indice</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Afficher l'indice</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}