Si vous avez passé un peu de temps sur React, vous avez forcément vu cet avertissement jaune dans votre console : « Each child in a list should have a unique ‘key’ prop. » Cela ressemble à une suggestion polie, mais React vous avertit en réalité qu'il ne parvient pas à distinguer vos éléments de liste. Ignorez-le, et vous finirez par livrer un bug extrêmement difficile à reproduire : un état qui saute sur la mauvaise ligne, des champs de texte qui perdent le focus ou des animations qui se déclenchent sur le mauvais élément.

Le moteur de rendu de React ne compare pas votre interface utilisateur pixel par pixel. Il construit un arbre d'objets léger appelé le virtual DOM, compare le nouvel arbre au précédent et calcule l'ensemble minimal de changements nécessaires pour le DOM réel. Lorsque vous rendez une liste, React voit un tableau d'éléments frères. Sans clés, il n'a aucun moyen fiable de savoir si un élément a été déplacé, remplacé ou supprimé. Par défaut, il se base sur la position, ce qui est fragile. Les clés agissent comme des identités stables. Elles disent à React : « Cet élément est le même qu'avant, même s'il se trouve maintenant dans un emplacement différent. » Si vous vous trompez, vous remplacez des mises à jour déterministes par de la pure conjecture.

La correction minimale

L'avertissement apparaît généralement à l'intérieur d'un appel map. Vous devez assigner une valeur unique à l'attribut key sur l'élément de premier niveau renvoyé par l'itérateur.

Voici le modèle que vous voyez dans chaque base de code qui déclenche l'avertissement :

const UserList = ({ users }) => {
  return (
    <ul>
      {users.map((user) => (
        <li>{user.name}</li>
      ))}
    </ul>
  );
};

React voit trois balises <li> et n'a aucune idée de laquelle est laquelle. La correction consiste en un seul attribut :

const UserList = ({ users }) => {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

La key doit être assignée à l'élément situé directement à l'intérieur du callback map. Si vous extrayez le <li> dans un composant UserItem séparé, la clé doit toujours être placée sur le composant au niveau de l'appel :

{users.map((user) => (
  <UserItem key={user.id} user={user} />
))}

Placer la clé à l'intérieur de UserItem sur son <div> interne ne fera pas taire l'avertissement et ne corrigera pas le comportement de réconciliation. React cherche la clé sur l'élément renvoyé par l'itérateur.

Pourquoi utiliser l'index comme clé est dangereux

Il est tentant de faire taire l'avertissement en utilisant le second argument de map :

{users.map((user, index) => (
  <li key={index}>{user.name}</li>
))}

Cela supprime le bruit dans la console, mais cela ne résout pas le problème de fond. Les indices de tableau ne sont pas des identités. Ce sont des positions, et les positions changent.

Imaginez une liste de trois utilisateurs rendus dans cet ordre :

  1. Alice (index 0)
  2. Bob (index 1)
  3. Charlie (index 2)

Si vous supprimez Alice, Bob passe à l'index 0 et Charlie passe à l'index 1. React compare le nouvel arbre à l'ancien. Il voit que l'index 0 contient désormais les données de Bob, il va donc muter le nœud DOM existant qui affichait précédemment Alice. Si ce nœud avait le focus, le curseur reste sur la première ligne, mais le texte devient celui de Bob. Si la ligne contenait un <input> avec un état local, cet état reste bloqué sur l'index 0. L'utilisateur tape dans ce qui semble être la ligne de Bob, mais l'état appartient à Alice. Le même chaos se produit lorsque vous triez, filtrez ou prépendez des éléments. Le seul cas où une clé basée sur l'index est sûre est une liste véritablement statique : sans réordonnancement, sans filtrage, sans insertion et sans suppression. Des liens de navigation codés en dur qui ne changent jamais en sont un bon exemple. Tout le reste nécessite un véritable identifiant.

Où trouver une clé stable

La meilleure clé est un identifiant unique qui existe déjà dans votre modèle de données. Les clés primaires de base de données comme id sont idéales car elles sont garanties uniques et survivent aux rendus. Si votre backend renvoie des objets avec un uuid, un slug ou un autre champ naturellement unique, utilisez celui-là.

Lorsque votre réponse API ne contient aucun champ unique, deux voies s'offrent à vous. Premièrement, parlez à votre équipe backend et demandez-leur d'inclure un id. Livrer des données relationnelles sans clé primaire est une mauvaise pratique, et la corriger à la source élimine l'ambiguïté dans toute votre pile technologique. Deuxièmement, si vous générez des éléments entièrement côté client — par exemple, une liste de tâches où les utilisateurs créent des tâches avant qu'elles n'atteignent le serveur — générez un ID une fois au moment de la création. Des bibliothèques comme uuid ou nanoid sont conçues exactement pour cela. Générez l'ID lorsque l'utilisateur soumet le formulaire, stockez-le sur l'objet, et utilisez-le comme clé pour toujours.

Ne générez jamais une clé à l'intérieur du chemin de rendu (render path). Appeler Math.random() ou Date.now() pendant le rendu d'un composant produit une nouvelle valeur à chaque passage. React voit une nouvelle clé, suppose qu'il s'agit d'un tout nouvel élément, détruit l'ancien nœud DOM et en crée un nouveau. Tout état à l'intérieur de cet élément est réinitialisé. Le focus est perdu. Les performances s'effondrent car React effectue un travail DOM inutile. Une clé générée aléatoirement est pire que l'absence totale de clé.

Fragments, composants et portée

Un piège moins évident concerne les Fragments React. Si vous effectuez un map sur des données et que vous devez retourner plusieurs éléments frères sans un conteneur <div>, vous pourriez être tenté d'utiliser la syntaxe courte :

{items.map((item) => (
  <>
    <dt>{item.term}</dt>
    <dd>{item.definition}</dd>
  </>
))}

La syntaxe abrégée <>...</> ne prend pas en charge les props, ce qui signifie que vous ne pouvez pas y attacher de clé. Dans ce cas, passez à la syntaxe explicite complète :

{items.map((item) => (
  <React.Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.definition}</dd>
  </React.Fragment>
))}

React a besoin de cette clé sur le Fragment pour pouvoir suivre la paire comme une unité unique à travers les rendus.

Un autre point subtil : les clés ne sont pas des props au sens habituel. Si vous écrivez <ListItem key={item.id} />, le composant ListItem ne peut pas lire props.key. React la consomme en interne pour sa gestion interne. Si votre composant a réellement besoin de l'identifiant pour sa propre logique, passez-le séparément sous un autre nom, tel que itemId.

Règles pratiques pour vous protéger

  • Privilégiez les IDs de base de données. Ils sont uniques, numériques ou sous forme de chaînes de caractères, et stables.
  • Utilisez uuid ou nanoid pour les données uniquement côté client. Générez l'ID une seule fois lors de la création de l'enregistrement, et non à l'intérieur du rendu du composant.
  • Ne déduisez jamais une clé de l'index du tableau si la liste peut changer. Le tri, le filtrage et la suppression introduiront des bugs visuels et d'état.
  • N'utilisez jamais Math.random(), Date.now() ou toute valeur qui change entre les rendus. Cela force un démontage et un remontage inutiles.
  • N'oubliez pas que les Fragments nécessitent la forme longue s'ils se trouvent à l'intérieur d'un map et requièrent une clé.
  • Placez la clé sur l'élément retourné par map, et non à l'intérieur d'un composant enfant.

Ce qu'il faut vraiment retenir

La prop key n'est pas une simple règle de lint décorative. C'est ainsi que React maintient l'identité à travers les rendus. Considérez-la comme une clé primaire dans une table de base de données. Lorsque cette identité est stable, React peut déplacer, mettre à jour et supprimer les éléments avec précision. Lorsqu'elle est absente ou instable, vous en payez le prix avec un état d'interface utilisateur corrompu et une réconciliation lente. Réglez le problème une fois pour toutes au niveau de la couche de données, et vos listes se comporteront de manière prévisible, peu importe leur croissance ou leurs changements.