Les clés React en tant qu’identité de composant : conservation et réinitialisation de l’état
Les clés React déterminent quelle instance de composant survit à un rendu. Comment les clés d’index font fuir l’état entre les lignes, et comment changer une clé réinitialise délibérément un formulaire.
Sources: React Keys Solve a Much Stranger Problem Than Rendering Lists
La plupart des développeurs React ajoutent une key à chaque appel à .map() sans y réfléchir, et la plupart des explications se limitent à dire que « React en a besoin pour suivre quel élément a changé ». Cette réponse est correcte mais incomplète. La vraie question que React se pose à chaque rendu est de savoir si l’élément qu’il voit maintenant est identique à celui qu’il a vu la dernière fois, et la key est le moyen par lequel vous répondez à cette question. Une fois que vous considérez les keys comme un mécanisme d’identification, les bugs liés à l’index-key deviennent prévisibles, et vous disposez d’une méthode déclarative et claire pour réinitialiser l’état d’un composant sur demande.
La version réflexive
C’est le schéma que l’on trouve dans presque tous les projets. Chaque utilisateur d’un tableau devient un élément User, et l’id de l’utilisateur sert de key:
users.map((user) => (
<User key={user.id} user={user} />
))
Il n’y a rien de mal dans ce fragment. Ce qui est intéressant, c’est ce qui se passe lorsque la clé est mal choisie, et cela ne se manifeste que lorsque les composants possèdent leur propre état.
Une ligne à état rend le problème visible
Considérez un composant carte qui conserve un drapeau local selected. Ce drapeau se trouve à l’intérieur du composant, et non dans la propriété user, ce qui oblige React à déterminer quelle instance de carte possède quel drapeau à chaque rendu :
interface UserCardProps {
user: User;
}
const UserCard = ({ user }: UserCardProps) => {
const [selected, setSelected] = useState(false);
return (
// ...
);
}
Maintenant, affichez trois personnes : Alice, Bob et un troisième utilisateur. Écrit sous forme de paires index-élément, le tableau ressemble à ceci :
0 → Alice
1 → Bob
2 → Maxime
Ce que React voit avec des clés d’index
Le conseil courant est d’éviter les indices de tableau comme clés. La raison devient évidente si l’on examine les éléments que React reçoit réellement lorsque l’index est utilisé :
<User key={0} user={alice} />
<User key={1} user={bob} />
<User key={2} user={maxime} />
Supposons que la liste permette de supprimer des éléments, d’en ajouter ou de réorganiser leur ordre par glisser-déposer. Si l’on supprime Alice, la prochaine mise à jour affiche ceci :
<User key={0} user={bob} />
<User key={1} user={maxime} />
De votre point de vue, Alice a disparu et les deux autres utilisateurs se sont déplacés vers le haut : Bob est passé de l’index 1 à l’index 0, et le troisième utilisateur de 2 à 1. Du point de vue de React, rien ne lui indique cela. Il constate qu’un élément avec key={0} existait auparavant et existe toujours, avec le même type de composant. Le même type associé au même identifiant est précisément le signal que React utilise pour décider s’il faut conserver une instance. Ainsi, il conserve l’instance qui affichait auparavant Alice et lui attribue les propriétés de Bob, conserve la deuxième instance et lui attribue les propriétés du troisième utilisateur, puis démonte la troisième instance.
La conséquence : si la carte d’Alice a été sélectionnée, la carte de Bob apparaît maintenant comme sélectionnée, car l’état selected appartenait à l’instance correspondant à la clé 0, et non à Alice. Ce même type de fuite affecte les entrées non contrôlées, le focus, la position de défilement à l’intérieur d’une ligne, ainsi que tout autre état lié à l’instance.
Pourquoi ces bugs échappent à la détection
Des clés incorrectes sont étonnamment fréquentes dans les applications réelles de toutes tailles. Une liste à laquelle on n’ajoute que des éléments sans jamais changer son ordre se comporte de manière identique avec des clés d’index et des clés stables, si bien qu’il n’y a aucun signe anormal. Le bug apparaît plus tard, lorsque l’on ajoute des fonctionnalités de suppression, de filtrage, de tri ou de réorganisation. Les outils de vérification aident : la règle react/no-array-index-key de eslint-plugin-react indique les clés d’index, permettant ainsi de prendre une décision consciemment plutôt que par habitude.
Lorsqu’un élément ne possède vraiment pas d’identifiant stable, il faut en générer un au moment de la création des données (par exemple avec crypto.randomUUID() au moment de l’insertion), et non pendant le rendu. Une clé générée à l’intérieur de .map() change à chaque rendu et force React à rémonter chaque ligne, ce qui est pire que d’utiliser l’index.
Les props décrivent le contenu, les clés décrivent l’identité
- les props répondent à la question "que contient cet élément en ce moment ?"
- la clé répond à la question ">de quel élément s’agit-il ?"
Les props peuvent changer librement tandis que le composant reste la même instance avec son état intact. Si une ligne permet l’édition en ligne, changer le nom d’un utilisateur ne doit pas amener React à supprimer cette ligne et à tout recommencer ; il s’agit toujours du même utilisateur. Une clé stable comme user.id exprime précisément cela.
Forcer délibérément une instance nouvelle
Parfois, vous souhaitez l’inverse : que React traite un composant comme s’il était entièrement nouveau. Une clé fonctionne également en dehors des listes :
<Profile key={user.id} user={user} />
Imaginez que Profile contienne un brouillon non enregistré dans l’état local :
interface ProfileProps {
user: User;
}
const Profile = ({ user }: ProfileProps) => {
const [draft, setDraft] = useState({...});
}
Si le composant parent passe d’un utilisateur à un autre, emporter le brouillon de l’utilisateur précédent dans le nouveau profil constituerait un véritable bug, voire une fuite de données. Sans clé, React considère qu’il s’agit du même type de composant à la même position et conserve l’instance, ce qui fait que le brouillon persiste. Avec key={user.id}, un nouvel identifiant indique à React que l’ancien Profile a disparu. Il démonte alors l’instance ancienne, jette son état, exécute les nettoyages associés à ses effets, puis monte une nouvelle instance dont les initialisateurs de useState s’exécutent à nouveau.
C’est l’alternative idiomatique à l’écriture d’un effet qui surveille user et réinitialise manuellement chaque élément d’état. Cette approche avec des effets affiche d’abord l’état obsolète avant de le corriger, et il est facile d’oublier un champ. Pour en savoir plus sur les raisons pour lesquelles la synchronisation de l’état via des effets a tendance à avoir des conséquences négatives, consultez le pattern plus sûr qui remplace la synchronisation basée sur useEffect.
Réinitialiser un formulaire à l’aide d’une clé
Même astuce permet une réinitialisation propre des formulaires :
<CreateUserForm key={draft.id} />
La clé agit comme une frontière d’identité. Chaque fois que draft.id change, par exemple après une soumission réussie qui crée un nouveau brouillon, React génère un nouveau CreateUserForm sans valeurs de champs restantes, erreurs de validation ou indicateurs d’interaction.
Deux points importants méritent d’être pris en compte. La remontée réinitialise tout le sous-arbre, y compris les enfants et tout ce qu’ils ont chargé lors du montage, il faut donc l’utiliser pour les composants où une réinitialisation complète est souhaitée. De plus, cela entraîne un démontage et un montage complets, ce qui est acceptable pour un formulaire mais pas quelque chose à déclencher à chaque frappe.
Où réside vraiment l’état
Tout cela découle de la manière dont React stocke l’état. L’état n’est pas lié à la fonction que vous avez écrite ; s’il l’était, chaque <User /> partagerait la même valeur selected. Au lieu de cela, React associe l’état à une position dans l’arbre rendu. Lors de chaque rendu, il parcourt le nouveau résultat et détermine, pour chaque élément, s’il s’agit d’une continuation de quelque chose provenant du rendu précédent. Le type du composant et sa position font partie de cette décision, et parmi les éléments frères, la clé la précise. L’article sur l’endroit où useState conserve sa valeur explore plus en détail la structure Fiber qui se cache derrière cela.
La règle qui en ressort est simple :
- même type, même position, même clé : React le considère comme le même composant et conserve son état
Les listes sont simplement le premier endroit où la position cesse d’être fiable. Lorsque les éléments peuvent se déplacer, apparaître ou disparaître, « deuxième enfant » ne signifie plus « la même chose que la dernière fois », c’est pourquoi React a besoin que vous fournissiez une identité. Une clé stable lui permet de suivre un élément même lorsque la liste autour change ; une clé modifiée est une indication explicite qu’un élément nouveau a pris sa place.
Points clés
- Une clé indique à React quelle instance correspond à un élément, et non simplement quel élément a changé.
- Les clés d’index lient l’état à une position spécifique, de sorte que la suppression ou le réordonnancement des éléments déplace l’état vers les lignes incorrectes.
- Utilisez des identifiants stables d’une rendu à l’autre ; ne générez jamais de clés pendant le rendu.
- Changer délibérément une clé est le moyen le plus propre de réinitialiser un composant, comme un formulaire ou un éditeur par entité.
- Utilisez la réinitialisation par clé avec modération : elle remonte tout le sous-arbre et jette tout ce qui s’y trouve.