Zustand
Kurz gesagt: Zustand ist ein Wert, den sich eine Komponente zwischen zwei Renderings merkt; in React wird er mit useState gehalten, und jede Änderung rendert die Komponente neu.
Problem
Der Button zählt nicht
Die Produktkarte hat einen Button „Hinzufügen“ und einen Zähler „Im Warenkorb“. Klick dreimal auf „Hinzufügen“.
Konsole
Die Konsole sagt count = 1, 2, 3. Die Karte sagt 0. Es gibt keinen Fehler. Die Variable count ändert sich, aber der Bildschirm folgt ihr nicht. Wo liegt der Unterschied?
Die Idee
Eine einfache Variable reicht nicht
Die Karte zeichnet nicht dein Code, sondern die Bibliothek. Sie zeichnet nur neu, wenn sie weiß, dass sich etwas geändert hat. count++ ändert die Variable, sagt es aber niemandem.
ProductCard ist eine Funktion. React ruft sie auf, wenn es die Karte zeichnet, und sie gibt zurück, was angezeigt werden soll. let count = 0 ist eine lokale Variable dieses Aufrufs. Der Klick ändert sie, aber niemand ruft die Funktion noch einmal auf, also bleibt die Karte bei 0.
Es gibt ein zweites Problem. Ruft React die Funktion aus einem anderen Grund noch einmal auf, ist die erste Zeile wieder let count = 0. Alles, was sich in der Variablen angesammelt hat, ist verloren.
Du brauchst Daten, die sich die Komponente zwischen zwei Renderings merkt und deren Änderung sie neu zeichnet. Das heißt Zustand (State).
Die Idee
useState und ref
const [count, setCount] = useState(0);
function add() { setCount(count + 1);}useState(0)gibt zwei Dinge zurück: den Wert und die Funktion, die ihn ändert. Die Null ist nur der Startwert.countist der Wert für dieses Rendering.setCount(1)merkt sich den neuen Wert und sagt React, dass es die Karte neu zeichnen soll. React ruftProductCardnoch einmal auf, und diesmal gibtuseState1 zurück.
useState ist ein Hook: eine React-Funktion, die oben in der Komponente aufgerufen wird, nie in einem if oder einer Schleife. React unterscheidet die Zustände an der Reihenfolge, in der sie aufgerufen werden.
Den Klick-Handler gibst du als Funktion an: onClick={add}. Schreibst du onClick={add()}, wird add aufgerufen, während die Karte gezeichnet wird. Es ändert den Zustand, der zeichnet neu, und React bricht mit dem Fehler „Too many re-renders“ ab.
Konsole
Sieh es dir an
Sieh, was bei einem Klick passiert
function ProductCard({ product }) { const [count, setCount] = useState(0);
function addThree() { setCount(count + 1); setCount(count + 1); setCount(count + 1); }
return ( <article> <button onClick={addThree}>+3</button> <p>Im Warenkorb: {count}</p> </article> );}Schritt für Schritt, in Worten
- Die Karte hält count im Zustand von React: useState(0) liefert den Wert und die Funktion, die ihn ändert.
- React ruft die Funktion ProductCard auf. Sie fasst die Seite nicht an, sondern gibt eine Beschreibung zurück: einen Button und einen Text mit count = 0.
- Beim ersten Mal wird die Beschreibung zu echten Elementen im DOM. Die Seite zeigt jetzt „Im Warenkorb: 0“.
- Ein Klick auf +3: Der Browser ruft addThree auf.
- Dreimal setCount(count + 1). In diesem Aufruf ist count eine Momentaufnahme: 0. Jedes Mal landet „werde 1“ in der Warteschlange. Der Bildschirm ändert sich noch nicht.
- Der Handler ist fertig. React wendet die Warteschlange an: 1, 1, 1 ergibt 1. Dann ruft es ProductCard noch einmal auf, und jetzt ist count 1.
- React vergleicht die neue Beschreibung mit der alten. Der Button ist derselbe, nur der Text hat sich geändert. Im DOM ändert sich nur der Text.
- Deshalb zeigt der Zähler 1, nicht 3. Hängt der neue Wert vom alten ab, übergib eine Funktion: setCount((c) => c + 1).
Die Karte wird nicht neu gebaut. Die Bibliothek vergleicht die neue Beschreibung mit der alten und ändert im DOM nur den Unterschied: den Text. Der Button ist dasselbe Element, und der Fokus bleibt auf ihm. Das ist der Unterschied zu render() im DOM-Modul, das alles ersetzt hat.
Probier es aus
Repariere den Zähler
Das ist die Karte vom Anfang. Mach count zum Zustand, damit die Karte nach jedem Klick auf „Hinzufügen“ die neue Zahl zeigt.
Konsole
Lösung
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Hinzufügen ${product.name}`}>
Hinzufügen
</button>
<p>Im Warenkorb: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 1, name: 'Weißbrot', price: 1.2 };
export default function App() {
return (
<main>
<h1>Der Shop</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
let count = 0 wird const [count, setCount] = useState(0), und aus count++ wird setCount(count + 1). useState muss importiert werden: import { useState } from 'react'. Bleibt count++ stehen, ändert sich der Wert, aber React zeichnet nicht neu.Die Idee
Der Wert ist eine Momentaufnahme
Mineralwasser gibt es auch in einer Packung mit 6 Flaschen. Praktisch wäre, wenn der Packungs-Button sechsmal add() aufruft. Hier verhalten sich die beiden Bibliotheken unterschiedlich.
count ist eine Konstante eines Renderings. setCount ändert count nicht in dem Handler, der gerade läuft. Es legt eine Anforderung in eine Warteschlange, und React wendet sie nach dem Handler an:
function addBox() { for (let i = 0; i < 6; i++) { setCount(count + 1); // 0 + 1, sechsmal: „werde 1“ }}Die sechs Anforderungen lauten „werde 1“, und der Zähler wird 1, nicht 6. Sag stattdessen „addiere 1 zum letzten Wert“: Übergib eine Funktion statt eines Werts.
setCount((c) => c + 1);React ruft jede solche Funktion mit dem Ergebnis der vorherigen auf: 0 → 1 → 2 … → 6. Die Regel: Hängt der neue Wert vom alten ab, übergib eine Funktion.
Probier es aus
Finde den Fehler
„Packung hinzufügen“ soll 6 Flaschen hinzufügen. Welche Zeile ist schuld?
Wähle die Zeile mit dem Fehler.
Zeile 5: count ist in jedem der sechs Aufrufe 0, also lauten die Anforderungen „werde 1“. Es müsste setCount((c) => c + 1) heißen oder gleich setCount((c) => c + 6).
Probier es aus
Eine Packung Wasser
Schreibe addBox() so, dass „Packung hinzufügen (6 Stk.)“ 6 Flaschen zu dem addiert, was schon im Warenkorb ist. Die Prüfung klickt einmal auf „Hinzufügen“, dann zweimal auf die Packung und erwartet 1, 7 und 13.
Konsole
Lösung
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount((c) => c + 1);
}
function addBox() {
setCount((c) => c + 6);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Hinzufügen ${product.name}`}>
Hinzufügen
</button>
<button type="button" onClick={addBox}>Packung hinzufügen (6 Stk.)</button>
<p>Im Warenkorb: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 15, name: 'Mineralwasser', price: 0.9 };
export default function App() {
return (
<main>
<h1>Der Shop</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
setCount((c) => c + 6) addiert 6 zum letzten Wert. Sechsmal setCount(count + 1) ergibt 2: count ist in jedem davon 1. setCount(6) addiert nicht, es setzt 6, und nach „Hinzufügen“ ist der Zähler 6, nicht 7. In der Lösung nutzt auch add eine Funktion: So hängt keiner der beiden Buttons von der Momentaufnahme ab.Die Idee
Jede Karte merkt sich ihren eigenen Wert
Der Zustand gehört zur Instanz, nicht zum Code. Der Katalog hat zwei Karten aus derselben ProductCard, und jede hat ihr eigenes count:
Konsole
Klick zweimal auf „Hinzufügen“ beim Brot und einmal bei der Milch: 2 und 1. Die Bibliothek hält den Zustand jeder Karte nach ihrem Platz im Baum der Komponenten. Nimmst du die Karte von der Seite, geht ihr Zustand mit. Wenn zwei Karten dieselbe Zahl zeigen müssen, muss sie in ihrem gemeinsamen Elternelement leben. Darum geht es in der Lektion über gemeinsamen Zustand in diesem Modul.
Genauer, für Neugierige
Warum eine Momentaufnahme. React ändert count nicht an Ort und Stelle, denn die Funktion wurde mit diesem Wert aufgerufen, und alles in ihr, auch die Handler, sieht diesen Wert. So ist ein Rendering in sich stimmig: Alles, was aus count berechnet wird, wird aus ein und demselben Wert berechnet.
Wann React nicht zeichnet. Bekommt setCount denselben Wert (verglichen mit Object.is), überspringt React das Rendering. Das ist in der nächsten Lektion bei Arrays wichtig.
Aktualisierungen werden gebündelt. Alle setCount-Aufrufe in einem Handler ergeben ein Rendering, und seit React 18 auch in setTimeout und nach await.
StrictMode. In der Entwicklung ruft <StrictMode> jede Komponente zweimal auf, um eine Komponente zu finden, die beim Zeichnen etwas außerhalb ändert. Die Sandbox nutzt ihn nicht, und ein Vite-Projekt hat ihn in main.jsx.
Klassenkomponenten. Älterer React-Code schreibt die Komponente als Klasse mit this.state und this.setState. Neuer Code nutzt Funktionen und Hooks.
Übung
Wähle die Menge
Der Katalog hat drei Karten. Füge in jeder eine Menge hinzu, die bei 1 beginnt, einen Button „−“ (Name für den Screenreader: „Weniger Weißbrot“), einen Button „+“ („Mehr Weißbrot“) und eine Zeile „2 × 1,20 € = 2,40 €“ mit Menge, Preis und Summe. „−“ ist deaktiviert, wenn die Menge 1 ist. Jede Karte zählt für sich.
Konsole
Lösung
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(1);
function less() {
setCount(count - 1);
}
function more() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<p>
<button type="button" onClick={less} disabled={count === 1} aria-label={`Weniger ${product.name}`}>−</button>
<button type="button" onClick={more} aria-label={`Mehr ${product.name}`}>+</button>
</p>
<p>{count} × {money(product.price)} = {money(count * product.price)}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
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() {
return (
<main>
<h1>Der Shop</h1>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
ProductCard: const [count, setCount] = useState(1), less ruft setCount(count - 1) auf, und more ruft setCount(count + 1) auf. „−“ ist disabled={count === 1}: So kann die Menge nicht unter 1 fallen. Die Summe ist money(count * product.price) direkt im JSX: Sie ergibt sich aus count und ist kein eigener Zustand. Darum geht es in der Lektion über abgeleiteten Zustand.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Ereignisse · Die erste Komponente · Props: Daten von oben · Listen und Keys
- Geht weiter in: Objekte und Arrays aktualisieren · Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Effekte
- Aufgaben zum Thema: Zustand