Objekte und Arrays aktualisieren
Kurz gesagt: Ein Objekt oder Array im React-Zustand wird mit einer neuen Kopie aktualisiert (Spread, map, filter), weil React Referenzen vergleicht und eine Änderung am alten nicht sieht.
Problem
„Entfernen“ entfernt nichts
Der Warenkorb hat drei Zeilen, jede mit einem Button „Entfernen“. Klick bei dem Brot auf „Entfernen“.
Konsole
Die Konsole sagt Zeilen: 2. Der Bildschirm zeigt alle drei Zeilen und die alte Summe. Die Daten haben sich geändert, der Bildschirm folgt ihnen nicht, und es gibt keinen Fehler.
remove nimmt die Zeile mit splice heraus und ruft setCart(cart) auf. Der neue Wert wurde übergeben. Warum zeichnet React nicht?Die Idee
Wie die Bibliothek eine Änderung sieht
Bei einer Zahl ist es klar: 1 und 2 sind verschieden. Ein Array und ein Objekt sind dagegen Referenzen: Die Variable zeigt auf ein Objekt im Speicher. Es gibt zwei Fragen. Zeigt die Variable auf ein anderes Objekt? Und hat sich der Inhalt desselben Objekts geändert? Jede Bibliothek fragt auf ihre Art, und hier zeigt sich der Unterschied im Code.
React stellt nur die erste Frage. Bei setCart(next) vergleicht es den alten und den neuen Zustand mit Object.is. Bei einem Array heißt das „ist es dasselbe Objekt“, nicht „hat es denselben Inhalt“.
splice ändert den Inhalt desselben Arrays. setCart(cart) übergibt dieselbe Referenz, Object.is sagt „dasselbe“, und React überspringt das Rendern. Deshalb ändert man in React das Array und die Objekte im Zustand nicht an Ort und Stelle. Du machst ein neues Array, und React sieht eine neue Referenz. Das heißt unveränderliches Aktualisieren (immutable update).
Sieh es dir an
Sieh: dasselbe Array oder ein neues
const [cart, setCart] = useState([bread]);
function addMilk() { cart.push(milk); setCart(cart);}
function addCheese() { setCart([...cart, cheese]);}Schritt für Schritt, in Worten
- cart zeigt auf ein Array mit einer Zeile. React hält diese Referenz als Zustand der Komponente, und der Bildschirm zeigt das Brot.
- cart.push(milk) fügt demselben Array eine Zeile hinzu. Der Inhalt hat sich geändert, aber es ist dasselbe Objekt.
- setCart(cart) übergibt dieselbe Referenz. React vergleicht die alte und die neue mit Object.is: dasselbe Objekt.
- Dasselbe heißt „nichts Neues“: React überspringt das Rendern. Die Milch steckt im Array, aber nicht auf dem Bildschirm.
- [...cart, cheese] macht ein neues Array: eine Kopie der Zeilen plus den Käse.
- setCart mit dem neuen Array: Object.is sagt „verschieden“, und React zeichnet neu.
- Jetzt zeigt der Bildschirm drei Zeilen. Auch die Milch, die bisher im alten Array versteckt war.
- Die Regel in React: Ändere das Array im Zustand nicht, gib ihm ein neues.
push im Array, erscheint auf dem Bildschirm aber erst zusammen mit dem Käse. Code mit einer Mutation scheitert nicht einfach. Er versteckt eine Änderung, die später herauskommt, bei einer ganz anderen Aktion.Die Idee
Ein neues Array, ein neues Objekt
Der Warenkorb hat drei Aktionen: eine Zeile hinzufügen, die Menge einer Zeile ändern, eine Zeile entfernen.
// hinzufügen: ein neues Array mit den alten Zeilen und der neuensetCart([...cart, { ...product, qty: 1 }]);
// eine Zeile ändern: ein neues Array, die geänderte Zeile ist ein neues ObjektsetCart(cart.map((line) => (line.id === id ? { ...line, qty: line.qty + 1 } : line)));
// entfernen: ein neues Array ohne die ZeilesetCart(cart.filter((line) => line.id !== id));[...cart, x]ist ein neues Array: die alten Elemente undxam Ende.{ ...line, qty: 5 }ist ein neues Objekt: die Felder vonline, mit geändertemqty.mapundfiltergeben immer ein neues Array zurück. Deshalb sind sie hier die wichtigsten Werkzeuge.
Probier es aus
Repariere „Entfernen“
Das ist der Warenkorb vom Anfang. Repariere remove so, dass die Zeile verschwindet und die Summe neu berechnet wird. Die Prüfung entfernt das Brot, dann den Käse.
Konsole
Lösung
App.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function App() {
const [cart, setCart] = useState([
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2 },
{ id: 5, name: 'Frischmilch', price: 1.6, qty: 1 },
{ id: 7, name: 'Weißkäse', price: 7.9, qty: 1 },
]);
function remove(id) {
setCart(cart.filter((line) => line.id !== id));
}
const total = cart.reduce((sum, line) => sum + line.price * line.qty, 0);
return (
<main>
<h2>Dein Warenkorb</h2>
<ul>
{cart.map((line) => (
<li key={line.id}>
{line.name}: {line.qty} × {money(line.price)}
<button type="button" onClick={() => remove(line.id)} aria-label={`Entfernen ${line.name}`}>
Entfernen
</button>
</li>
))}
</ul>
<p>Gesamt: {money(total)}</p>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
setCart(cart.filter((line) => line.id !== id)): filter gibt ein neues Array ohne die Zeile zurück, und React sieht es. const next = cart ist keine Kopie: next und cart zeigen auf dasselbe Array, und splice auf next ist splice auf dem Zustand.Probier es aus
Finde den Fehler
„Entfernen“ entfernt nichts. Welche Zeile ist schuld?
Wähle die Zeile mit dem Fehler.
Zeile 2: const next = cart kopiert das Array nicht, es gibt demselben Array einen zweiten Namen. splice ändert den Zustand an Ort und Stelle, und setCart(next) übergibt dieselbe Referenz. Es müsste setCart(cart.filter((line) => line.id !== id)) heißen.
Die Idee
Eine Zeile des Arrays
Die Kundin drückt bei dem Brot auf „+“. Das qty eines Objekts muss sich ändern, und das Objekt steckt in einem Array.
cart.find(...).qty++ ändert das Objekt an Ort und Stelle: Es ist dasselbe Objekt, das React als alten Zustand hält. Das Array ist dasselbe, also zeichnet React nicht neu. Die Zahl geht nicht verloren: Sie erreicht den Bildschirm beim nächsten Rendern aus einem anderen Grund, und die Menge „springt“.
Vorsicht auch bei Kopien. [...cart] ist ein neues Array, aber die Zeilen darin sind dieselben Objekte. Das ist eine flache Kopie (shallow copy): Nur die erste Ebene wird kopiert. copy[0].qty++ ändert die Zeile auch im alten Zustand. Die geänderte Zeile muss ein neues Objekt sein: { ...line, qty: line.qty + 1 }. Die Zeilen, die sich nicht ändern, bleiben dieselben Objekte, und das ist in Ordnung.
Genauer, für Neugierige
Object.is. Wie ===, mit zwei Unterschieden: Object.is(NaN, NaN) ist true, und Object.is(0, -0) ist false. Bei Arrays und Objekten vergleichen beide Referenzen.
Verschachtelte Objekte. Jede Ebene auf dem Weg zur Änderung ist neu: { ...order, address: { ...order.address, city: 'Varna' } }. Die Ebenen daneben bleiben dieselben.
structuredClone(cart) macht eine tiefe Kopie: das Array und jedes Objekt darin. Das funktioniert, kopiert aber bei jeder Änderung alles, und Funktionen kann es nicht kopieren. Für eine Zeile sind map und ... billiger.
Immer. Die Bibliothek Immer lässt dich schreiben, als würdest du mutieren (draft[0].qty++), und macht die neuen Objekte für dich. Sie ist nicht in der Sandbox.
Es funktioniert auch in Vue. Das neue Array ist auch in Vue richtig: cart.value = cart.value.filter(...). Umgekehrt gilt es nicht: Die Mutation, die in Vue normal ist, wird in React nicht gesehen.
Probier es aus
„+“ und „−“
Schreibe more und less. „+“ erhöht die Menge der Zeile um 1, und „−“ verringert sie um 1. Ist die Menge 1, entfernt „−“ die Zeile. Die Summe folgt der Menge.
Konsole
Lösung
App.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function App() {
const [cart, setCart] = useState([
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2 },
{ id: 5, name: 'Frischmilch', price: 1.6, qty: 1 },
{ id: 7, name: 'Weißkäse', price: 7.9, qty: 1 },
]);
function more(id) {
setCart(cart.map((line) => (line.id === id ? { ...line, qty: line.qty + 1 } : line)));
}
function less(id) {
const line = cart.find((l) => l.id === id);
if (line.qty === 1) {
remove(id);
} else {
setCart(cart.map((l) => (l.id === id ? { ...l, qty: l.qty - 1 } : l)));
}
}
function remove(id) {
setCart(cart.filter((line) => line.id !== id));
}
const total = cart.reduce((sum, line) => sum + line.price * line.qty, 0);
return (
<main>
<h2>Dein Warenkorb</h2>
<ul>
{cart.map((line) => (
<li key={line.id}>
{line.name}: {line.qty} × {money(line.price)}
<button type="button" onClick={() => less(line.id)} aria-label={`Weniger ${line.name}`}>−</button>
<button type="button" onClick={() => more(line.id)} aria-label={`Mehr ${line.name}`}>+</button>
<button type="button" onClick={() => remove(line.id)} aria-label={`Entfernen ${line.name}`}>
Entfernen
</button>
</li>
))}
</ul>
<p>Gesamt: {money(total)}</p>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
more ist setCart(cart.map((line) => (line.id === id ? { ...line, qty: line.qty + 1 } : line))). less findet die Zeile und ruft bei qty === 1 remove auf, sonst macht es dasselbe map mit - 1. Schreibst du line.qty++ und setCart(cart), zeichnet React nicht: Das Array und die Zeile sind dieselben. Ohne die Prüfung auf 1 erreicht die Zeile „0 ד und bleibt im Warenkorb.
Übung
Katalog und Warenkorb
Der Katalog hat vier Produkte mit einem Button „Hinzufügen“ (der Name für den Screenreader: „Hinzufügen Weißbrot“). Schreibe add und remove. Der erste Klick auf „Hinzufügen“ macht eine Zeile mit der Menge 1, und die nächsten erhöhen die Menge derselben Zeile, statt eine neue zu machen. „Entfernen“ entfernt die Zeile. Ist der Warenkorb leer, steht dort „Der Warenkorb ist leer“.
Konsole
Lösung
App.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const products = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 10, name: 'Äpfel', price: 2.1 },
];
export default function App() {
const [cart, setCart] = useState([]);
function add(product) {
const found = cart.find((line) => line.id === product.id);
if (found) {
setCart(cart.map((line) => (line.id === product.id ? { ...line, qty: line.qty + 1 } : line)));
} else {
setCart([...cart, { ...product, qty: 1 }]);
}
}
function remove(id) {
setCart(cart.filter((line) => line.id !== id));
}
const total = cart.reduce((sum, line) => sum + line.price * line.qty, 0);
return (
<main>
<h1>Der Shop</h1>
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name}, {money(p.price)}
<button type="button" onClick={() => add(p)} aria-label={`Hinzufügen ${p.name}`}>
Hinzufügen
</button>
</li>
))}
</ul>
<h2>Dein Warenkorb</h2>
{cart.length === 0 && <p>Der Warenkorb ist leer</p>}
<ul aria-label="Dein Warenkorb">
{cart.map((line) => (
<li key={line.id}>
{line.name}: {line.qty} × {money(line.price)}
<button type="button" onClick={() => remove(line.id)} aria-label={`Entfernen ${line.name}`}>
Entfernen
</button>
</li>
))}
</ul>
<p>Gesamt: {money(total)}</p>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
add sucht die Zeile mit find. Ist sie da: map mit einem neuen Objekt für sie. Wenn nicht: setCart([...cart, { ...product, qty: 1 }]). { ...product, qty: 1 } ist ein neues Objekt, und das Produkt im Katalog bleibt unberührt. remove ist filter. Mit cart.push und setCart(cart) bleibt der Warenkorb leer, und mit [...cart, …] fügt jeder Klick eine neue Zeile für dasselbe Produkt hinzu, und zwei Zeilen teilen sich einen key.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Die erste Komponente · Listen und Keys · Bedingungen: leer, Fehler, lädt · Zustand
- Geht weiter in: Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Gemeinsamer Zustand · Das Schaufenster des Shops
- Aufgaben zum Thema: Zustand