Gemeinsamer Zustand: nach oben
Kurz gesagt: Beim Hochziehen des Zustands (lifting state up) wandert der gemeinsame Zustand zum nächsten gemeinsamen Elternelement: Daten kommen als Props nach unten, und Änderungen gehen über einen Callback nach oben.
Problem
Der Zähler oben weiß von nichts
Der Shop hat oben einen Warenkorb-Zähler und darunter eine Produktliste. Lege Brot, Milch und noch ein Brot hinein.
Konsole
Die Brotkarte sagt „Im Warenkorb: 2“, und oben steht „Warenkorb (0)“. Der Warenkorb lebt in ProductList: Dort wird der Button gedrückt, und dort wurde der Zustand angelegt. CartBadge ist eine andere Komponente, ein Geschwister von ProductList, und hat eine eigene Zahl, die niemand ändert. Die beiden Komponenten haben zwei Warenkörbe, und jede hat mit ihrem eigenen recht.
Die Idee
Hinauf zum gemeinsamen Elternelement
Eine Komponente sieht nur ihren eigenen Zustand und die Props, die ihr Elternelement ihr gibt. Ein Geschwister sieht sein Geschwister nicht. Zwei Stellen, die einen Warenkorb zeigen, können also nicht jeweils ihren eigenen haben.
Die Lösung: Der Zustand zieht zum nächsten gemeinsamen Elternelement der beiden. Hier ist das App. Das heißt Zustand nach oben holen (lifting state up):
Apphältitemsund berechnetcount.- Daten kommen als Props nach unten:
countzuCartBadge,itemszuProductList. - Änderungen gehen als Nachricht nach oben: Der Button in
ProductCardändert den Warenkorb nicht, er sagtApp: „Füge dieses Produkt hinzu“.
export default function App() { const [items, setItems] = useState([]); const count = items.reduce((sum, i) => sum + i.qty, 0); function add(product) { /* ändert items */ } return ( <> <header><CartBadge count={count} /></header> <main><ProductList products={PRODUCTS} items={items} onAdd={add} /></main> </> );}onAdd={add} reicht die Funktion nach unten. Das Kind ruft sie auf, wenn etwas passiert: onClick={() => onAdd(product)}. Eine solche Funktion heißt Callback.
Jetzt haben CartBadge und ProductList keinen eigenen Zustand für den Warenkorb mehr. Sie zeigen, was App ihnen gibt.
Sieh es dir an
Sieh zu, wie er nach oben wandert
Zuerst liegt der Warenkorb in ProductList. Dann wandert er nach App und fließt von dort nach unten.
function App() { const [items, setItems] = useState([]); const count = items.reduce((s, i) => s + i.qty, 0); function add(product) { setItems(plusOne(items, product)); } return ( <> <CartBadge count={count} /> <ProductList items={items} onAdd={add} /> </> );}
function ProductList({ items, onAdd }) { return PRODUCTS.map((p) => ( <ProductCard key={p.id} product={p} qty={qtyOf(items, p)} onAdd={onAdd} /> ));}
function ProductCard({ product, qty, onAdd }) { return <button onClick={() => onAdd(product)}>…</button>;}Schritt für Schritt, in Worten
- CartBadge und ProductList sind Geschwister: Beide sind Kinder von App. Zuerst liegt der Warenkorb in ProductList, und CartBadge hat eine eigene Zahl.
- Ein Klick auf „Hinzufügen“ erreicht ProductList, und die Liste zählt 1. CartBadge sieht diesen Zustand nicht: Ein Geschwister kann den Zustand seines Geschwisters nicht lesen. Es bleibt bei 0.
- Die Lösung: Zieh den Warenkorb zum nächsten gemeinsamen Elternelement der beiden, App. Der Zähler gibt seine eigene Kopie auf.
- Von App fließen die Daten als Props nach unten: count zu CartBadge, items zu ProductList und qty zu jeder ProductCard. Beide Geschwister lesen jetzt einen Warenkorb.
- Das Kind besitzt den Warenkorb nicht, also kann es ihn nicht ändern. Der Klick ruft onAdd(product) auf: eine Funktion, die App nach unten gereicht hat. Der Aufruf wandert nach oben zu add() in App.
- App ändert items, und die neuen Props fließen nach unten: Der Zähler sagt 2, die Karte sagt 2. Niemand hat sie von Hand aktualisiert.
- Die Regel: Der Zustand lebt an einer Stelle, so hoch wie nötig und nicht höher. Daten gehen als Props nach unten, Änderungen gehen als Ereignisse nach oben.
Die Richtung ist in jeder App dieselbe: Daten nach unten, Nachrichten nach oben. Wem die Daten gehören, der allein ändert sie.
Probier es aus
Hol den Warenkorb nach oben
Der Warenkorb liegt in ProductList, und CartBadge zählt eine eigene Zahl. Zieh den Warenkorb nach App: CartBadge bekommt die Anzahl als Prop, ProductList bekommt items und meldet „Hinzufügen“ nach oben. Die Prüfung legt Brot, Milch und Brot hinein und schaut auf die Karten und den Zähler.
Konsole
Lösung
App.jsximport { useState } from 'react';
import CartBadge from './CartBadge.jsx';
import ProductList from './ProductList.jsx';
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 },
];
export default function App() {
const [items, setItems] = useState([]);
const count = items.reduce((sum, i) => sum + i.qty, 0);
function add(product) {
const found = items.find((i) => i.id === product.id);
if (found) setItems(items.map((i) => (i.id === product.id ? { ...i, qty: i.qty + 1 } : i)));
else setItems([...items, { ...product, qty: 1 }]);
}
return (
<>
<header>
<a href="/">Der Shop</a>
<CartBadge count={count} />
</header>
<main>
<h1>Produkte</h1>
<ProductList products={PRODUCTS} items={items} onAdd={add} />
</main>
</>
);
}
CartBadge.jsxexport default function CartBadge({ count }) {
return <a href="/cart">Warenkorb ({count})</a>;
}
ProductList.jsximport ProductCard from './ProductCard.jsx';
export default function ProductList({ products, items, onAdd }) {
const qtyOf = (id) => items.find((i) => i.id === id)?.qty ?? 0;
return (
<ul>
{products.map((p) => (
<ProductCard key={p.id} product={p} qty={qtyOf(p.id)} onAdd={onAdd} />
))}
</ul>
);
}
ProductCard.jsximport { money } from './money.js';
export default function ProductCard({ product, qty, onAdd }) {
return (
<li>
<span>{product.name}, {money(product.price)}</span>
{qty > 0 && <span>Im Warenkorb: {qty}</span>}
<button type="button" aria-label={`Hinzufügen ${product.name}`} onClick={() => onAdd(product)}>
Hinzufügen
</button>
</li>
);
}
money.jsexport const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span:first-child { flex: 1; }
button { font: inherit; }
items, add und count ziehen nach App. CartBadge bekommt count={count}, ProductList bekommt items={items} und onAdd={add} und reicht onAdd an jede ProductCard weiter. In ProductList gibt es kein useState mehr.
Die Prüfung fängt auch onClick={onAdd(product)} in ProductCard ab. Ohne den Pfeil wird onAdd schon beim Rendern aufgerufen. Es ändert App, App rendert neu, die Karte ruft onAdd wieder auf, und so weiter ohne Ende. Der Zähler wächst von selbst ohne einen einzigen Klick, und die Konsole meldet Cannot update a component (`App`) while rendering a different component (`ProductCard`).
Die Idee
Das Kind meldet, das Elternelement entscheidet
ProductCard weiß nicht, wie der Warenkorb gebaut ist. Sie weiß nur, dass jemand bei ihrem Produkt auf „Hinzufügen“ gedrückt hat. Also meldet sie, was passiert ist, nicht, was zu ändern ist: „füge dieses Produkt hinzu“, nicht „erhöhe qty in Zeile 2“. App entscheidet, was das bedeutet: eine neue Zeile oder eine Einheit mehr.
Das hält die Karte einfacher, und sie kann überall stehen: im Katalog, bei „Empfohlen“, in der Suche. Sie besitzt die Daten nicht und fasst sie nicht an.
Die Namen folgen einer Konvention: Eine Prop mit einer Funktion beginnt mit on und benennt das Ereignis: onAdd, onRemove, onChange. Die Funktion im Elternelement heißt oft add oder handleAdd. Du übergibst die Funktion, nicht ihr Ergebnis: onClick={() => onAdd(product)}, nicht onClick={onAdd(product)}.
Probier es aus
Fülle die Lücke
Was gehört in die Lücke, damit der Klick das Elternelement erreicht?
export default function ProductCard({ product, onAdd }) {
return <button onClick={() => (product)}>Hinzufügen</button>;
}onAdd: die Funktion, die das Elternelement als Prop übergeben hat. Der Pfeil ruft sie erst beim Klick auf, mit dem Produkt der Karte.Probier es aus
Finde den Fehler
Wähle die Zeile mit dem Fehler.
onClick={onAdd(product)} ruft onAdd bei jedem Rendern der Karte auf und gibt onClick das Ergebnis. Es müsste onClick={() => onAdd(product)} heißen: eine Funktion, die beim Klick aufgerufen wird.Die Idee
Wenn der Baum tief wird
Wie weit nach oben? So hoch wie nötig und nicht höher. Wenn nur ProductCard etwas benutzt (ob die Beschreibung geöffnet ist), bleibt es dort. Du ziehst es nach oben, wenn eine zweite Komponente es sehen muss.
In einem echten Shop braucht man den Warenkorb an vielen Stellen: den Zähler oben, die Liste, die Produktseite, die Bestellung. Das gemeinsame Elternelement liegt weit oben, und der Weg zu ihnen führt durch Komponenten, die den Warenkorb nicht benutzen, sondern ihn nur nach unten reichen. Das heißt Prop Drilling. Bei zwei Ebenen ist es normal. Bei fünf Ebenen muss jede Komponente auf dem Weg von Daten wissen, die ihr nicht gehören.
Dafür gibt es eigene Werkzeuge: Context. Sie geben die Daten an weit entfernte Nachfahren weiter, ohne die Komponenten dazwischen. Sie stehen in der Lektion über gemeinsamen Zustand im Modul „Effekte, Daten und Formulare“. Die Regel bleibt gleich: Die Daten haben einen Besitzer, und Änderungen laufen über ihn.
Genauer, für Neugierige
Props sind schreibgeschützt. Im Entwicklungsmodus friert React das Props-Objekt ein: props.qty = 3 wirft TypeError: Cannot assign to read only property 'qty'. Ein Kind, das etwas ändern will, meldet nach oben.
Wozu ein Pfeil. onClick will eine Funktion, die React später aufruft. onClick={add} ist auch eine Funktion, aber React ruft sie mit dem Klick-Ereignis auf, nicht mit dem Produkt. Also onClick={() => onAdd(product)}: eine neue Funktion, die das Produkt kennt.
Klassenkomponenten haben dasselbe Modell: Das Elternelement übergibt onAdd als Prop, und das Kind ruft this.props.onAdd(product) auf.
useState(items) eine eigene Kopie, übernimmt die Kopie nur den ersten Wert und folgt dem Elternelement nicht mehr. Das ist der Fehler aus der Lektion über abgeleiteten Zustand.Übung
Der Warenkorb an der Seite
Neben der Liste gibt es einen dritten Teil, CartSummary: die Überschrift „Dein Warenkorb“, Zeilen wie „Weißbrot × 2“ mit einem Button „Entfernen“, „Gesamt: 4,00 €“ und „Der Warenkorb ist leer“, wenn er leer ist. Im Moment hält CartSummary einen eigenen Warenkorb, den niemand füllt. Gib ihm den Warenkorb aus App und sorge dafür, dass „Entfernen“ daraus entfernt. Die Prüfung legt drei Produkte hinein, entfernt sie und schaut auf den Zähler, die Karten und die Summe.
Konsole
Lösung
App.jsximport { useState } from 'react';
import CartBadge from './CartBadge.jsx';
import ProductList from './ProductList.jsx';
import CartSummary from './CartSummary.jsx';
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 },
];
export default function App() {
const [items, setItems] = useState([]);
const count = items.reduce((sum, i) => sum + i.qty, 0);
function add(product) {
const found = items.find((i) => i.id === product.id);
if (found) setItems(items.map((i) => (i.id === product.id ? { ...i, qty: i.qty + 1 } : i)));
else setItems([...items, { ...product, qty: 1 }]);
}
function remove(item) {
setItems(items.filter((i) => i.id !== item.id));
}
return (
<>
<header>
<a href="/">Der Shop</a>
<CartBadge count={count} />
</header>
<main>
<h1>Produkte</h1>
<ProductList products={PRODUCTS} items={items} onAdd={add} />
<CartSummary items={items} onRemove={remove} />
</main>
</>
);
}
CartBadge.jsxexport default function CartBadge({ count }) {
return <a href="/cart">Warenkorb ({count})</a>;
}
ProductList.jsximport ProductCard from './ProductCard.jsx';
export default function ProductList({ products, items, onAdd }) {
const qtyOf = (id) => items.find((i) => i.id === id)?.qty ?? 0;
return (
<ul>
{products.map((p) => (
<ProductCard key={p.id} product={p} qty={qtyOf(p.id)} onAdd={onAdd} />
))}
</ul>
);
}
ProductCard.jsximport { money } from './money.js';
export default function ProductCard({ product, qty, onAdd }) {
return (
<li>
<span>{product.name}, {money(product.price)}</span>
{qty > 0 && <span>Im Warenkorb: {qty}</span>}
<button type="button" aria-label={`Hinzufügen ${product.name}`} onClick={() => onAdd(product)}>
Hinzufügen
</button>
</li>
);
}
CartSummary.jsximport { money } from './money.js';
export default function CartSummary({ items, onRemove }) {
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
return (
<section aria-labelledby="cart-title">
<h2 id="cart-title">Dein Warenkorb</h2>
{items.length === 0 && <p>Der Warenkorb ist leer</p>}
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name} × {item.qty}</span>
<button type="button" aria-label={`Entfernen ${item.name}`} onClick={() => onRemove(item)}>
Entfernen
</button>
</li>
))}
</ul>
<p>Gesamt: {money(total)}</p>
</section>
);
}
money.jsexport const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span:first-child { flex: 1; }
button { font: inherit; }
App bekommt eine Funktion remove(item), die items filtert. CartSummary bekommt items={items} und onRemove={remove}, und der Button ruft onRemove(item) auf. Die Summe in CartSummary wird aus items berechnet. Jetzt lesen der Zähler, die Karten und der Warenkorb an der Seite einen Zustand, also zeigt sich das Entfernen an allen drei Stellen.
CartSummary ab (useState(props.items)): Sie ist beim Anlegen leer und sieht die später hinzugefügten Produkte nicht.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Bedingungen: leer, Fehler, lädt · Zustand · Objekte und Arrays aktualisieren · Abgeleiteter Zustand
- Geht weiter in: Gemeinsamer Zustand · Routing · Geschwindigkeit: wann eine Komponente neu rendert · Komponenten testen
- Aufgaben zum Thema: Zustand