Zum Inhalt
wannacode

Listen und Keys

Kurz gesagt: Eine Liste in React entsteht mit map aus einem Array von Komponenten, und key sagt React, welche alte Zeile welche neue ist; eine Zufallszahl ist ein schlechter Key, und der Index ist es auch, sobald sich die Liste ändert.

Problem

Ich habe das Brot entfernt, und die Milch wurde 3

Der Käufer hat Brot, Milch und Käse im Warenkorb. Er tippt 3 für das Brot und 2 für die Milch ein und entfernt dann das Brot. Probier es aus: Ändere die beiden Zahlen und klick auf „Entfernen“ neben dem Brot.

React

Das Brot ist weg, aber die Milch steht jetzt auf 3 und der Käse auf 2. In der Konsole steht kein Fehler. Die Daten stimmen: Entfernt wurde genau das Brot. Nur die Felder sind durcheinander. Schuld ist ein einziges Attribut in Zeile 20 von App.jsx: das, was der Code der Bibliothek über jede Zeile der Liste sagt.

Die Idee

Eine Liste in einer Komponente

Im Modul „JavaScript in der Seite“ wurde eine Liste aus einem Array mit map und createElement gebaut. In einer Komponente ist die Idee dieselbe, aber jedes Element des Arrays wird zu einem Teil des Markups:

<ul>
{items.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>

map gibt ein Array von JSX-Elementen zurück, und React zeichnet sie nacheinander. Jedes Element des Arrays hat ein Attribut key: den Key, an dem React es wiedererkennt.

Wenn sich die Daten ändern, gibt die Komponente eine neue Liste zurück. Die Bibliothek löscht die alte nicht, um sie neu zu zeichnen. Sie vergleicht die alte mit der neuen und ändert auf der Seite nur den Unterschied. Deshalb bleiben der Fokus und das in Felder Eingetippte erhalten, statt verloren zu gehen wie bei render() im Modul „JavaScript in der Seite“.

Zum Vergleichen muss sie wissen, welche alte Zeile zu welcher neuen gehört. Genau das sagt der Key.

Die Idee

Zuordnung über den Key

Die Bibliothek ordnet alte und neue Zeilen über den Key zu:

  • Derselbe Key ist dieselbe Zeile. Das Element auf der Seite bleibt, und die Bibliothek ändert darin nur, was sich in den Daten unterscheidet: den Namen, den Preis.
  • Ein Key, den die neue Liste nicht mehr hat, ist eine entfernte Zeile. Ihr Element wird von der Seite entfernt.
  • Ein neuer Key ist eine neue Zeile. Es entsteht ein neues Element.

Das Element, das bleibt, behält alles, was ihm gehört: das in ein <input> Eingetippte, den Fokus, ein geöffnetes Dropdown. Das steht nicht in den Daten, sondern im Element selbst. Deshalb ist es wichtig, dass es bei seinem Produkt bleibt.

Im Beispiel ist der Key der Index: die Position im Array. Nach „Entfernen“ steht die Milch an Position 0. Die alte Zeile mit dem Key 0 gehörte zum Brot. Die Bibliothek behält sie, ändert ihren Namen in „Frischmilch“ und lässt das Feld mit der 3 stehen.

Sieh es dir an

Die Zuordnung ansehen

Links ist die Seite vor „Entfernen“, rechts die neue Liste aus der Komponente. Achte darauf, welche Zeile verschwindet.

// key = die Position in der Liste
{items.map((p, index) => (
<li key={index}>
<ProductCard product={p} />
</li>
))}
// key = die id des Produkts
{items.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
Schritt für Schritt, in Worten
  1. Drei Zeilen im Warenkorb, und der Key ist die Position: 0, 1, 2. Der Käufer hat 3, 2 und 1 in die Felder getippt. Das Eingetippte lebt im Element <input>, nicht in den Daten.
  2. „Entfernen Weißbrot“. Die Daten enthalten jetzt zwei Produkte, und die Komponente gibt eine neue Liste zurück: Key 0 ist die Milch, Key 1 ist der Käse.
  3. Die Bibliothek ordnet die alte und die neue Liste über den Key zu: 0 mit 0, 1 mit 1. Key 2 fehlt in der neuen Liste, also verschwindet die dritte Zeile: die vom Käse.
  4. Die behaltenen Zeilen bekommen die neuen Daten, und ihre Überschriften lauten jetzt Milch und Käse. Die Felder sind die alten, mit 3 und 2. Die Anzahl vom Brot steht jetzt bei der Milch.
  5. Derselbe Warenkorb mit key = id. Die Keys sind 1, 2, 3 und gehören zum Produkt, nicht zur Position.
  6. Noch einmal „Entfernen Weißbrot“. Die neue Liste hat die Keys 2 und 3.
  7. Key 2 wird mit Key 2 zugeordnet und 3 mit 3, obwohl sie nach oben gerückt sind. Key 1 fehlt, also verschwindet die Zeile vom Brot, zusammen mit ihrem Feld.
  8. Jedes Feld ist bei seinem Produkt geblieben: Milch 2, Käse 1. Der Key sagt der Bibliothek, wer wer ist, nicht, wo er steht.

Beim Index verschwindet die letzte Zeile, und die ersten beiden ändern ihre Namen. Bei der id verschwindet die Zeile vom Brot, und die anderen beiden rücken mit ihren Feldern einfach nach oben. In beiden Fällen sind die Daten dieselben. Der Unterschied ist, was der Key sagt: die Position oder das Produkt.

Probier es aus

Repariere den Key

Repariere den Warenkorb so, dass nach „Entfernen“ jede Zahl bei ihrem Produkt bleibt. Die Prüfung tippt 3 für das Brot und 2 für die Milch ein, entfernt das Brot und schaut sich die Felder von Milch und Käse an.

React

Probier es aus

Ergänze die Lücke

Wie heißt das Attribut, an dem die Bibliothek eine Zeile erkennt?

{items.map((p) => (
  <li ={p.id}>
    <ProductCard product={p} />
  </li>
))}

Die Idee

Ein guter Key

Ein guter Key ist stabil und eindeutig: derselbe für dasselbe Produkt bei jedem Zeichnen und ein anderer für andere Produkte. Am besten ist eine id aus den Daten: Sie kommt aus der Datenbank und ändert sich nicht.

  • Der Index ist ein schlechter Key, wenn sich die Liste ändert: beim Entfernen, beim Einfügen am Anfang, beim Sortieren, beim Filtern. Er ist in Ordnung, wenn die Liste nur gelesen wird und ihre Reihenfolge nie ändert.
  • Math.random() ist immer ein schlechter Key. Bei jedem Zeichnen ist der Key neu, also hat keine alte Zeile ein Gegenstück. Die Bibliothek entfernt alle Zeilen und baut neue. Die Felder verlieren das Eingetippte, und der Fokus verschwindet.
  • Zwei gleiche Keys bringen die Zuordnung durcheinander. Hier ist der Key der Name, und „Joghurt“ gibt es in zwei Größen. Klick auf „Nach Preis sortieren“ und zähl die Zeilen.
React

Es sind vier Produkte, und die Zeilen werden zu fünf: Ein Joghurt ist doppelt da.

React sagt das schon beim ersten Zeichnen, mit einem Fehler in der Konsole: „Encountered two children with the same key, Joghurt“. Wenn du den Key vergisst, lautet der Fehler „Each child in a list should have a unique "key" prop.“ Beide Meldungen kommen von React im Entwicklungsmodus. Auf der echten Seite schweigt die Konsole, und der Fehler auf der Seite bleibt.

Probier es aus

Finde den Fehler

Bei jedem Zeichnen springen alle „Anzahl“-Felder auf 1 zurück, und der Cursor verlässt das Feld.

Wähle die Zeile mit dem Fehler.

Die Idee

Filtern und Sortieren

Der Katalog zeigt nur die Produkte, die verfügbar sind, nach Preis sortiert. Das ist eine abgeleitete Liste: Sie wird nicht getrennt gespeichert, sondern bei jedem Zeichnen aus den Daten gebaut.

const visible = PRODUCTS
.filter((p) => p.stock > 0)
.sort((a, b) => a.price - b.price);

Das steht im Rumpf der Komponente: Jedes Zeichnen baut visible neu aus PRODUCTS.

  • filter gibt ein neues Array nur mit den verfügbaren Produkten zurück. Das Original bleibt ganz.
  • sort sortiert das Array an Ort und Stelle. Hier ist das sicher, weil es nach filter kommt, das schon eine Kopie geliefert hat. Ohne Filter machst du vorher eine Kopie: [...PRODUCTS].sort(…).
  • Der Key bleibt p.id. Beim Sortieren wechseln die Zeilen ihre Plätze, und der Index würde den Key jeder einzelnen ändern.

Probier es aus

Nur das, was verfügbar ist

Zeige nur die verfügbaren Produkte (stock > 0), nach Preis sortiert, das günstigste zuerst. Schreib über die Liste „Verfügbar: 3 von 5“.

React
Genauer, für Neugierige

key ist keine Prop. Die Bibliothek nimmt den Key für sich und gibt ihn nicht an die Komponente weiter. props.key in ProductCard ist undefined, und React schreibt den Fehler „key is not a prop“. Wenn die Komponente die id braucht, gib sie getrennt weiter: product enthält sie schon.

Ein Key zum Zurücksetzen. Ein neuer Key heißt ein neues Element. Ein geänderter Key ist also eine Möglichkeit, eine Komponente zurückzusetzen: Ein Formular mit der id des Produkts als Key startet für jedes neue Produkt leer.

Mehrere Elemente pro Zeile. Wenn ein Produkt aus zwei Elementen besteht (<dt> und <dd>), kommt der Key an die Hülle: <Fragment key={p.id}>…</Fragment>. Die Kurzform <>…</> nimmt keinen Key.

Klassenkomponenten. Älterer React-Code hat Komponenten als Klassen (class ProductList extends Component). Keys funktionieren darin genauso.

toSorted. products.toSorted((a, b) => a.price - b.price) gibt eine sortierte Kopie zurück und rührt das Original nicht an. Es gibt es in Browsern seit 2023.

Warum das Feld „unkontrolliert“ ist. Hier steht die Zahl im Feld nur im DOM. Im Modul „Zustand“ wird das Feld kontrolliert: Die Zahl steht in den Daten und folgt ihnen. Dann spielt der Key trotzdem eine Rolle: für den Fokus, den Cursor und den Zustand, den die Komponente in sich selbst hält.

Übung

Der Warenkorb

Der Zustand und der Button „Nach Preis sortieren“ sind fertig. Baue die Zeile für jedes Produkt: ProductCard und einen Button „Entfernen“, dessen Name für einen Screenreader „Entfernen Weißbrot“ ist. Die Prüfung tippt Anzahlen ein, sortiert, entfernt das Brot und schaut, ob jede Zahl bei ihrem Produkt bleibt.

React

Test

Teste dich

1. Woran erkennt die Bibliothek, welche Zeile der alten Liste welche Zeile der neuen ist?
2. Der Key ist der Index. Der Käufer entfernt das erste Produkt. Was passiert mit den Feldern der anderen Zeilen?
3. Warum ist Math.random() ein schlechter Key?
4. Die Liste soll nach Preis sortiert sein. Wie?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·