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

  • 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 déterminent si l’état est préservé ou non

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

React construit un arbre de rendu pour représenter la structure des composants de votre UI.

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 rendu.

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 sous forme d’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 a 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 comprendre comment ça fonctionne.

Dans React, chaque composant à l’écran a son propre état complétement isolé. Par exemple, si vous affichez 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 constatez qu’ils ne s’affectent pas l’un 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 afficherez le même composant à la même position dans l’arbre. Pour vous en rendre compte, incrémentez les deux compteurs, puis supprimez le deuxième composant en décochant « Afficher le deuxième compteur », et 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 cessez d’afficher le 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.

Suppression d’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é à sa position dans l’arbre de l’UI. S’il est supprimé, ou si un composant différent est affiché à 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 <Counter /> différentes :

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 cochez 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 contenant « isFancy » (qui est affichée en violet), laquelle 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 contenant « isFancy » (qui est affichée en violet), laquelle 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 à la même position

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

Piège

Souvenez-vous que c’est 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 est réinitialisé quand vous cochez la case, mais ce n’est pas le cas ! C’est parce 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 d’un rendu à l’autre, 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, cliquer 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 basculez ici entre deux types de composants différents à 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 affichiez 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ée « 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ée « 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 « correspondre » 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éée à chaque rendu de MyComponent. Puisque vous affichez composant différent à la même position, React réinitialise tout l’état en dessous. Ça cause des bugs et des problèmes de performances. Pour éviter ce problème, déclarez toujours les fonctions de composants au niveau racine, et n’imbriquez pas leurs définitions.

Réinitialiser l’état à 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 surveiller 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 apparaissent certes à 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. Afficher les composants à deux positions différentes.
  2. Donner explicitement à chaque composant une identité avec key.

Option 1 : changer la position du composant

Si vous souhaitez rendre ces deux Counter indépendants, vous pouvez choisir de les afficher à 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, tandis que la seconde position 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é « 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é « Counter » et contient une bulle d'état appelée « count » avec une valeur à 0.

Appui sur « Joueur suivant »

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é.
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é.

Nouvel appui 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 position dans l’arbre. 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 de l’affichage des listes. Ces clés ne sont pas réservées aux listes ! Vous pouvez les utiliser pour aider React à faire la distinction entre n’importe quels composants. Par défaut, React utilise l’ordre dans un parent (« premier compteur », « deuxième compteur ») pour différencier les composants. 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 reconnaîtra 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 position 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éserve 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, encore et encore.

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é s’avère 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 affiché à 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 toutes les discussions plutôt que seulement celle qui est active, mais en masquant les autres avec du CSS. Les discussions 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 en réalité 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 affiché à 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 lui 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 disparaît

Cet exemple affiche un message quand vous appuyez sur le bouton. Cependant, appuyer sur ce bouton vide aussi le champ de saisie par accident. Pourquoi ? 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)}
    />
  );
}