Startseite / Artikel

React Keys als Komponentenidentität: Zustand beibehalten und zurücksetzen

Die React-Keys entscheiden, welche Komponenteninstanz nach einem Render erhalten bleibt. Wie Index-Keys den Zustand zwischen Zeilen durchsickern lassen und wie das Ändern eines Keys absichtlich ein Formular zurücksetzt.

Verlag: REACTAPP.TOP6 Min. Lesezeit1462 Wörter

Quellen: React Keys Solve a Much Stranger Problem Than Rendering Lists

Die meisten React-Entwickler fügen ohne weiteres Nachdenken einer jeden .map()-Aufruf eine key hinzu, und die meisten Erklärungen beschränken sich auf den Satz „React benötigt sie, um zu verfolgen, welcher Eintrag sich geändert hat.“ Diese Antwort ist zwar richtig, aber unvollständig. Die eigentliche Frage, die React bei jeder Renderung stellt, ist, ob das Element, das es jetzt sieht, dasselbe ist wie beim letzten Mal – und die key dient dazu, diese Frage zu beantworten. Sobald man keys als Identitätsmechanismus betrachtet, werden Fehler mit index-key vorhersehbar, und man erhält eine saubere, deklarative Möglichkeit, den Zustand eines Components nach Bedarf zurückzusetzen.

Die reflexive Version

Hier ist das Muster, das in fast jedem Codebase vorkommt. Jeder Benutzer in einem Array wird zu einem User-Element, wobei die ID des Benutzers als key dient:

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

Mit diesem Codeausschnitt ist alles in Ordnung. Interessant wird es, wenn die Schlüsselwahl unglücklich ist – und das zeigt sich erst dann, wenn die Komponenten ihren eigenen Zustand speichern.

Ein zustandsbasiertes Element macht das Problem sichtbar

Betrachten wir eine Kartenkomponente, die ein lokales selected-Flagge speichert. Diese Flagge befindet sich innerhalb der Komponente und nicht im user-Prop, weshalb React bei jeder Neurenderung entscheiden muss, welche Karteninstanz welches Flagge besitzt:

interface UserCardProps {
  user: User;
}

const UserCard = ({ user }: UserCardProps) => {
  const [selected, setSelected] = useState(false);

  return (
    // ...
  );
}

Rendern wir nun drei Personen: Alice, Bob und einen dritten Benutzer. Als Paare aus Index und Element sieht der Array so aus:

0 → Alice
1 → Bob
2 → Maxime

Was React mit Index-Schlüsseln sieht

Der gängige Rat lautet, Array-Indizes als Schlüssel zu vermeiden. Der Grund wird klar, wenn man sich die Elemente ansieht, die React tatsächlich erhält, wenn Indizes verwendet werden:

<User key={0} user={alice} />
<User key={1} user={bob} />
<User key={2} user={maxime} />

Nehmen wir an, die Liste unterstützt Löschung, Hinzufügen oder Umordnung per Drag-and-Drop. Wenn Alice entfernt wird, ergibt die nächste Darstellung Folgendes:

<User key={0} user={bob} />
<User key={1} user={maxime} />

Aus Ihrer Sicht ist Alice verschwunden und die anderen beiden nach oben gerückt. Bob ist vom Index 1 auf den Index 0 gewandert, und der dritte Benutzer von 2 auf 1. Aus Sicht von React gibt es jedoch keinen Hinweis darauf. Es erkennt, dass ein Element mit key={0} zuvor existierte und weiterhin existiert, wobei derselbe Komponententyp verwendet wird. Derselbe Typ in Kombination mit demselben Key ist genau das Signal, das React verwendet, um zu entscheiden, ob eine Instanz beibehalten werden soll. Daher behält React die Instanz bei, die ursprünglich Alice angezeigt hat, und weist ihr Bobs Eigenschaften zu; es behält die zweite Instanz bei und weist ihr die Eigenschaften des dritten Benutzers zu, während die dritte Instanz abgemontet wird.

Die Folge: Wenn Alices Karte ausgewählt wurde, erscheint nun Bobs Karte als ausgewählt, weil der selected-Zustand der Instanz mit Schlüssel 0 gehörte und nicht Alice. Derselbe Art von Fehler tritt bei unkontrollierten Eingaben, Fokussierung, Scrollposition innerhalb einer Zeile sowie jedem anderen mit der Instanz verbundenen Zustand auf.

Warum diese Fehler durchschlüpfen

Falsche Schlüssel sind in echten Anwendungen jeder Größe überraschend häufig. Eine Liste, zu der nur Elemente hinzugefügt werden und deren Reihenfolge niemals geändert wird, verhält sich identisch mit Index-Schlüsseln und stabilen Schlüsseln, sodass nichts falsch erscheint. Der Fehler zeigt sich erst später, wenn Löschfunktionen, Filter, Sortierungen oder Neuordnungen hinzugefügt werden. Tools zur Codeprüfung helfen dabei: Die Regel react/no-array-index-key aus eslint-plugin-react weist auf Index-Schlüssel hin, sodass die Entscheidung bewusst statt aus Gewohnheit getroffen wird.

Falls ein Element tatsächlich keine stabile ID hat, sollte diese bei Erstellung der Daten generiert werden (zum Beispiel mit crypto.randomUUID() zum Zeitpunkt der Einfügung) und nicht während der Darstellung. Eine im .map() generierte Schlüsselwert ändert sich bei jeder Darstellung und zwingt React dazu, jede Zeile erneut anzuzeigen – was schlimmer ist als die Verwendung des Indexes.

Props beschreiben den Inhalt, Schlüsselwerte beschreiben die Identität

Ein kompaktes mentales Modell hilft hier:

  • Props beantworten die Frage "Was enthält dieses Element gerade?"
  • Der Schlüsselwert beantwortet die Frage "Um welches Element handelt es sich?"

Props können frei geändert werden, während das Komponentenobjekt weiterhin dieselbe Instanz mit unverändertem Zustand bleibt. Wenn eine Zeile die Inline-Bearbeitung unterstützt, sollte ein Namewechsel eines Benutzers nicht dazu führen, dass React die Zeile löscht und von vorne anfängt – es handelt sich immer noch um denselben Benutzer. Ein stabiler Schlüsselwert wie user.id drückt genau das aus.

Bewusste Erzwingung einer neuen Instanz

Mannchmal möchte man das Gegenteil: Man will, dass React ein Komponente wie brandneu behandelt. Eine Schlüsselwerte funktionieren auch außerhalb von Listen:

<Profile key={user.id} user={user} />

Stellen Sie sich vor, Profile enthält einen ungespeicherten Entwurf im lokalen Zustand:

interface ProfileProps {
  user: User;
}

const Profile = ({ user }: ProfileProps) => {
  const [draft, setDraft] = useState({...});
}

Falls der Elternteil von einem Benutzer auf einen anderen wechselt, wäre es ein echter Fehler, den Entwurf des vorherigen Benutzers in das nächste Profil mitzunehmen – möglicherweise sogar ein Datenleck. Ohne Schlüsselwert erkennt React denselben Komponententyp an derselben Stelle und behält die Instanz bei, sodass der Entwurf erhalten bleibt. Mit key={user.id} gibt ein neuer ID an React mit, dass die alte Profile-Instanz nicht mehr vorhanden ist. React deinstalliert die alte Instanz, löscht ihren Zustand, führt die Cleanup-Funktionen aus und installiert eine neue Instanz, bei der die Initialisierungen von useState erneut ausgeführt werden.

Dies ist die übliche Alternative zur Erstellung eines Effects, der user überwacht und den Zustand manuell zurücksetzt. Der Ansatz mit Effects führt zunächst mit veraltetem Zustand aus, bevor dieser korrigiert wird, und es ist leicht, ein Feld zu übersehen. Weitere Informationen dazu, warum das Synchronisieren des Zustands über Effects oft kontraproduktiv ist, finden Sie unter dem sichereren Muster, das das auf useEffect basierende Synchronisieren ersetzt.

Form mit einem Schlüssel zurücksetzen

Derselbe Trick ermöglicht ein sauberes Zurücksetzen von Formularen:

<CreateUserForm key={draft.id} />

Der Schlüssel dient als Identitätsgrenze. Immer wenn sich beispielsweise draft.id ändert – nach einer erfolgreichen Übermittlung, die einen neuen Entwurf erstellt – baut React eine neue CreateUserForm ohne verbleibende Feldwerte, Validierungsfehler oder Markierungen für bearbeitete Felder auf.

Zwei Punkte sollten man im Hinterkopf behalten. Das Erneute Montieren setzt den gesamten Unterbaum zurück, einschließlich der Kinderkomponenten und allem, was sie beim Montieren heruntergeladen haben – nutzen Sie es daher nur für Komponenten, bei denen tatsächlich ein vollständiger Neustart gewünscht ist. Zudem erfordert es ein vollständiges Entmontieren und Neumontieren, was für ein Formular in Ordnung ist, aber nicht etwas sein sollte, das bei jeder Tastenbetätigung ausgelöst wird.

Wo der Zustand tatsächlich gespeichert ist

Alles dies ergibt sich aus der Art und Weise, wie React den Zustand speichert. Der Zustand ist nicht an die von Ihnen geschriebene Funktion gebunden; wäre er es, würde jedes <User /> denselben selected-Wert teilen. Stattdessen verknüpft React den Zustand mit einer Position im gerenderten Baum. Bei jeder Neuerstellung durchläuft er die neue Ausgabe und entscheidet für jedes Element, ob es eine Fortsetzung von etwas aus der vorherigen Neuerstellung ist. Der Komponententyp und die Position sind Teil dieser Entscheidung, wobei der Schlüssel bei Geschwisterelementen diese Entscheidung präzisiert. Der Artikel zu wo useState seinen Wert behält geht tiefer auf die dahinterliegende Fiber-Struktur ein.

Die daraus resultierende Regel ist einfach:

  • derselbe Typ, dieselbe Position, derselbe Schlüssel: React betrachtet es als dieselbe Komponente und behält ihren Zustand bei
  • eine andere Art oder ein anderer Schlüssel: React verwirft den alten Zustand und erstellt eine neue Instanz
  • Listen sind einfach der erste Ort, an dem die Position nicht mehr zuverlässig ist. Wenn Elemente sich bewegen, erscheinen oder verschwinden, bedeutet „zweites Kind“ nicht mehr „dasselbe wie beim letzten Mal“, weshalb React von Ihnen eine Identifikation verlangt. Ein stabiler Schlüssel ermöglicht es ihm, ein Element auch dann weiterzuverfolgen, wenn sich die Liste um es herum ändert; ein geänderter Schlüssel ist eine explizite Angabe, dass etwas Neues seinen Platz eingenommen hat.

    Hauptpunkte

    • Ein Schlüssel teilt React mit, um welche Instanz es sich bei einem Element handelt – nicht nur, welches Element sich geändert hat.
    • Index-Schlüssel verknüpfen den Zustand mit einer bestimmten Position, wodurch das Löschen oder Umordnen von Elementen dazu führt, dass der Zustand auf falsche Zeilen übertragen wird.
    • Verwenden Sie IDs, die bei mehreren Darstellungen stabil bleiben; erzeugen Sie niemals Schlüssel während der Darstellung.
    • Die absichtliche Änderung einer Schlüsselwerte ist die sauberste Methode, um einen Komponenten wie ein Formular oder einen Editor pro Entity zurückzusetzen.
    • Setzen Sie den Wert über den Schlüssel nur sparsam ein: Dadurch wird der gesamte Unterbaum neu geladen und alles darin verworfen.