Abgeleiteter Zustand
Kurz gesagt: Abgeleiteter Zustand ist ein Wert, der beim Rendern aus dem Zustand berechnet wird (eine Anzahl, eine Summe, eine gefilterte Liste), statt getrennt gespeichert zu werden; bei einer teuren Berechnung behält useMemo das Ergebnis zwischen Renderings.
Problem
Die Summe fällt zurück
Der Warenkorb enthält Weißbrot und Frischmilch. Klick beim Brot auf „+“: Die Anzahl und die Summe wachsen. Entferne jetzt die Milch.
Konsole
Die Zeile verschwindet, die Anzahl sinkt auf 3, und die Summe bleibt bei 5,20 €. Die Komponente hält drei Zustände: items, count und total. Jeder Klick muss alle drei ändern, und die Funktion zum Entfernen ändert zwei. Das ist dieselbe Abweichung wie in der Lektion „Eine Liste aus Daten“, nur innerhalb einer Komponente. Die Bibliothek zeichnet genau das, was du ihr gibst. Du gibst ihr zwei Wahrheiten über einen Warenkorb.
Die Idee
Berechnen, nicht behalten
Frag bei jedem Stück Zustand: Kann ich es aus dem anderen Zustand oder aus den Props berechnen? Wenn ja, ist es kein Zustand. Die Anzahl ist die Summe der Mengen in items. Die Summe ist die Summe aus Preis mal Menge. Beides wird berechnet.
Ein solcher Wert heißt abgeleiteter Zustand (derived state). Du hältst nur items und berechnest den Rest daraus, jedes Mal, wenn die Komponente rendert.
const [items, setItems] = useState(START);const count = items.reduce((sum, i) => sum + i.qty, 0);const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);setItems lässt React die Funktion App noch einmal ausführen. Dann werden count und total aus den neuen items berechnet. Es sind einfache Konstanten. Sie brauchen kein useState und nichts Besonderes.
Jetzt ändern die Funktionen more und remove nur items. Es bleibt nichts mehr, was sie vergessen könnten.
Sieh es dir an
Sieh die berechneten Werte und die Kopie
Links sind der Warenkorb und eine Kopie der Summe, getrennt gehalten. Rechts sind die drei berechneten Werte.
const [items, setItems] = useState(START);const [savedTotal, setSavedTotal] = useState(26.9);
const count = items.reduce((s, i) => s + i.qty, 0);const total = items.reduce((s, i) => s + i.price * i.qty, 0);const free = total >= 30;
function more(item) { setItems(items.map((i) => i.id === item.id ? { ...i, qty: i.qty + 1 } : i, )); setSavedTotal(savedTotal + item.price);}
function remove(item) { setItems(items.filter((i) => i.id !== item.id));}Schritt für Schritt, in Worten
- Links ist der Zustand: die Warenkorb-Einträge items. Darunter ist ein zweiter Zustand, savedTotal: dieselbe Summe, als Kopie gehalten.
- Rechts stehen count, total und free: einfache Konstanten. React führt App bei jedem Rendern aus, also werden sie jedes Mal neu aus items berechnet: 26,90 €, bis zur kostenlosen Lieferung fehlen 3,10 €.
- more(Käse): items ändern sich, und die drei Pfeile leuchten auf. Die Summe ist 34,80 €, die Lieferung ist kostenlos. more() aktualisiert auch savedTotal von Hand, also stimmt die Kopie noch.
- remove(Käse): items ändern sich, und count, total und free folgen von selbst: 3,20 €. Für savedTotal in remove() hat niemand eine Zeile geschrieben.
- Die Kopie sagt 34,80 €, der Warenkorb ist 3,20 €. Kein Fehler, nur eine falsche Zahl: Der Zustand und seine Kopie sind auseinandergelaufen.
- Die Regel: Halte nur, was sich nicht berechnen lässt. Was sich aus dem Zustand ergibt, wird daraus berechnet, und es gibt keine Kopie, die man vergessen könnte.
Die berechneten Werte folgen dem Warenkorb bei jeder Änderung, weil sie kein eigenes Gedächtnis haben. Die Kopie folgt nur, solange jemand daran denkt, sie zu aktualisieren. Die erste Funktion, die es vergisst, macht sie falsch.
Probier es aus
Berechne Anzahl und Summe
Der Warenkorb hält count und total als eigene Zustände, und remove() vergisst die Summe. Entferne beide Zustände und berechne die Anzahl und die Summe aus items. Die Prüfung klickt beim Brot auf „+“, entfernt dann die Milch und schaut auf die Anzahl und die Summe.
Konsole
Lösung
App.jsximport { useState } from 'react';
import { money } from './money.js';
const START = [
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2 },
{ id: 5, name: 'Frischmilch', price: 1.6, qty: 1 },
];
export default function App() {
const [items, setItems] = useState(START);
// Anzahl und Summe: bei jedem Rendern berechnet.
const count = items.reduce((sum, i) => sum + i.qty, 0);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
function more(item) {
setItems(items.map((i) => (i.id === item.id ? { ...i, qty: i.qty + 1 } : i)));
}
function remove(item) {
setItems(items.filter((i) => i.id !== item.id));
}
return (
<main>
<h1>Dein Warenkorb</h1>
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name} × {item.qty}</span>
<button type="button" aria-label={`Mehr ${item.name}`} onClick={() => more(item)}>+</button>
<button type="button" aria-label={`Entfernen ${item.name}`} onClick={() => remove(item)}>Entfernen</button>
</li>
))}
</ul>
<p>Artikel: {count}</p>
<p>Gesamt: {money(total)}</p>
</main>
);
}
money.jsexport const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button { font: inherit; }
button[aria-pressed="true"] { background: #2f6f4f; color: white; }
Es bleibt nur items. Die Anzahl ist items.reduce((sum, i) => sum + i.qty, 0), die Summe ist dasselbe mit i.price * i.qty. In React sind das Konstanten im Körper von App: Sie werden bei jedem Rendern berechnet. more und remove ändern jetzt nur den Warenkorb.
Eine Falle: const [total] = useState(items.reduce(…)). Der Anfangswert von useState wird nur beim ersten Rendern genommen, deshalb bleibt die Summe für immer 4,00 €.
Probier es aus
Fülle die Lücke
Die Anzahl wird aus dem Warenkorb berechnet. Was gehört in die Lücke?
const [items, setItems] = useState(START);
count = items.reduce((sum, i) => sum + i.qty, 0);const: eine einfache Konstante im Körper der Komponente. Sie wird bei jedem Rendern neu berechnet, während useState hier nur den ersten Wert behalten würde.Die Idee
Wann man das Ergebnis behält
Die Konstante wird bei jedem Rendern der Komponente berechnet, auch wenn items gleich sind und das Rendern von einem anderen Zustand kommt. Bei einer Summe über zehn Zeilen ist das nichts. Ist die Berechnung teuer (Tausende Produkte sortieren), hat React useMemo:
const sorted = useMemo(() => products.toSorted(byPrice), [products]);React behält das Ergebnis und führt die Funktion nur dann erneut aus, wenn sich eine der Abhängigkeiten im Array ([products]) vom vorigen Rendern unterscheidet. Vergisst du eine Abhängigkeit, bleibt das Ergebnis alt. Deshalb ist useMemo nicht für jede Summe da. Fang mit einer Konstante an und ergänze es, wenn du misst, dass die Berechnung langsam ist. Das Ergebnis ist dasselbe, es wird nur seltener berechnet.
Die Idee
Auch der Filter wird berechnet
Der Katalog hat die Buttons „Alle“, „Brot“ und „Milch“. Die Versuchung ist, zwei Arrays zu halten: alle Produkte und die sichtbaren. Aber die sichtbaren ergeben sich aus zwei anderen Tatsachen: der vollständigen Liste und der gewählten Kategorie. Du hältst nur die Kategorie:
const [category, setCategory] = useState('all');const visible = category === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category);Beim Sortieren ist es dasselbe: Du hältst „nach Preis“ oder „nach Name“, und die sortierte Liste wird berechnet. Vorsicht bei sort(): Es sortiert das Array an Ort und Stelle, das heißt, es ändert den Zustand. toSorted() gibt ein neues Array zurück und lässt das alte in Ruhe.
Probier es aus
Nur die Kategorie
Die Buttons ändern die Kategorie, aber die Liste zeigt immer alles. Berechne die sichtbaren Produkte aus der Kategorie und zeige über der Liste „Angezeigt: 3 von 5“. Die Prüfung wählt „Milch“, dann „Brot“, dann „Alle“.
Konsole
Lösung
App.jsximport { useState } from 'react';
import ProductCard from './ProductCard.jsx';
const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2, category: 'bread' },
{ id: 2, name: 'Vollkornbrot', price: 2.1, category: 'bread' },
{ id: 5, name: 'Frischmilch', price: 1.6, category: 'milk' },
{ id: 6, name: 'Joghurt', price: 0.9, category: 'milk' },
{ id: 7, name: 'Weißkäse', price: 7.9, category: 'milk' },
];
const CATEGORIES = [
{ id: 'all', name: 'Alle' },
{ id: 'bread', name: 'Brot' },
{ id: 'milk', name: 'Milch' },
];
export default function App() {
const [category, setCategory] = useState('all');
const visible = category === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category);
return (
<main>
<h1>Produkte</h1>
{CATEGORIES.map((c) => (
<button key={c.id} type="button" aria-pressed={category === c.id} onClick={() => setCategory(c.id)}>
{c.name}
</button>
))}
<p>Angezeigt: {visible.length} von {PRODUCTS.length}</p>
<ul>
{visible.map((p) => <ProductCard key={p.id} product={p} />)}
</ul>
</main>
);
}
ProductCard.jsximport { money } from './money.js';
export default function ProductCard({ product }) {
return (
<li>
<span>{product.name}</span> {money(product.price)}
</li>
);
}
money.jsexport const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button { font: inherit; }
button[aria-pressed="true"] { background: #2f6f4f; color: white; }
visible wird aus category und PRODUCTS berechnet: Bei „Alle“ ist es die ganze Liste, sonst ein filter nach Kategorie. Die Anzahl ist visible.length von PRODUCTS.length.
Ist visible ein zweiter Zustand, wird der Code oft zu diesem: setCategory(id) und gleich danach setVisible(… category …). Aber category ist in diesem Klick noch der alte Wert: Der neue kommt mit dem nächsten Rendern. Die Liste hinkt einen Klick hinterher. Mit einem berechneten Wert gibt es dieses Problem nicht.
Die Idee
Eine Kopie einer Prop
Derselbe Fehler passiert zwischen Komponenten. App gibt 20 % Rabatt und übergibt die neuen Preise an jede ProductCard. Die Karte aber merkt sich den Preis, den sie beim ersten Mal bekommen hat:
Konsole
Klick auf „20 % Rabatt“. Der Button ist gedrückt, und das Brot kostet immer noch 1,20 €.
const [price] = useState(product.price) nimmt product.price nur beim ersten Rendern der Karte. Danach rendert React die Karte mit dem neuen product, aber ihr Zustand ist ihr eigener und bleibt.
Die Lösung ist, nicht zu kopieren: Zeig product.price direkt an. Soll die Karte etwas zeigen, das aus einer Prop berechnet wird (den Preis mit Mehrwertsteuer, den Preis pro Einheit), berechne es beim Rendern.
Probier es aus
Finde den Fehler
Die Käuferin drückt „+“: Die Menge in der Zeile wird 3, und die Zeilensumme bleibt wie bei 2. Welche Zeile ist schuld?
Wähle die Zeile mit dem Fehler.
item.price * item.qty in den Zustand. useState nimmt den Anfangswert einmal, deshalb folgt die Zeilensumme der Menge nicht. Es müsste const subtotal = item.price * item.qty; heißen.Genauer, für Neugierige
Wenn die Kopie Absicht ist. Manchmal willst du einen Wert von außen nur als Start: Ein Bearbeitungsformular beginnt mit dem aktuellen Namen, und dann tippt die Person. Dann ist die Kopie richtig, aber sag es im Namen: die Prop initialName, der Zustand name. Wer den Code liest, sieht, dass Änderungen von außen nicht verfolgt werden.
useMemo ist ein Hinweis. React behält sich das Recht vor, das gemerkte Ergebnis zu verwerfen und neu zu berechnen, und die Dokumentation sagt das ausdrücklich. Der Code muss also auch ohne useMemo richtig sein. Der React Compiler, falls das Projekt ihn nutzt, merkt sich solche Berechnungen selbst, und ein manuelles useMemo wird noch seltener gebraucht.
Renderings sind häufig. Eine Komponente rendert neu, wenn sich ihr Zustand ändert oder wenn ihr Elternelement rendert. Die Konstanten darin werden jedes Mal berechnet. Deshalb gehören in den Körper einer Komponente kein fetch, keine Timer und keine Zustandsänderungen: Dafür sind Effekte da, das nächste Modul.
Dasselbe gilt für Daten vom Server. Gibt der Server die Produkte zurück und du hältst „Anzahl der Produkte“ getrennt, können die beiden nach einem neuen Laden auseinanderlaufen. Halte die Antwort und berechne die Anzahl.
Übung
Kostenlose Lieferung
Der Warenkorb hat die Buttons „−“ und „+“ an jeder Zeile („−“ bei der letzten Einheit entfernt die Zeile). Zeige unter der Liste „Artikel: 4“, „Gesamt: 18,20 €“ und „Bis zur kostenlosen Lieferung fehlen noch 11,80 €“, und ab 30 € (FREE_FROM) nur „Die Lieferung ist kostenlos“. Alles wird aus items berechnet. Die Prüfung legt Käse bis 34,00 € hinein, geht dann wieder unter 30 € und entfernt das Brot.
Konsole
Lösung
App.jsximport { useState } from 'react';
import { money } from './money.js';
const START = [
{ id: 7, name: 'Weißkäse', price: 7.9, qty: 2 },
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2 },
];
const FREE_FROM = 30;
export default function App() {
const [items, setItems] = useState(START);
const count = items.reduce((sum, i) => sum + i.qty, 0);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const free = total >= FREE_FROM;
function more(item) {
setItems(items.map((i) => (i.id === item.id ? { ...i, qty: i.qty + 1 } : i)));
}
function less(item) {
if (item.qty === 1) setItems(items.filter((i) => i.id !== item.id));
else setItems(items.map((i) => (i.id === item.id ? { ...i, qty: i.qty - 1 } : i)));
}
return (
<main>
<h1>Dein Warenkorb</h1>
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name} × {item.qty}</span>
<button type="button" aria-label={`Weniger ${item.name}`} onClick={() => less(item)}>−</button>
<button type="button" aria-label={`Mehr ${item.name}`} onClick={() => more(item)}>+</button>
</li>
))}
</ul>
<p>Artikel: {count}</p>
<p>Gesamt: {money(total)}</p>
{free ? <p>Die Lieferung ist kostenlos</p> : <p>Bis zur kostenlosen Lieferung fehlen noch {money(FREE_FROM - total)}</p>}
</main>
);
}
money.jsexport const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button { font: inherit; }
button[aria-pressed="true"] { background: #2f6f4f; color: white; }
Die Anzahl, die Summe und free werden aus items berechnet; die Lieferungsmeldung ist bedingtes Rendern: free oder der Restbetrag FREE_FROM - total. Die Prüfung geht über 30 € und wieder darunter. Deshalb wird ein getrennt gehaltenes „free“ erwischt: Es wird bei „+“ zu true, aber bei „−“ setzt es niemand zurück. Aus demselben Grund erwischt die Prüfung auch useMemo(…, []): Das leere Array sagt „nie wieder berechnen“.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Listen und Keys · Bedingungen: leer, Fehler, lädt · Zustand · Objekte und Arrays aktualisieren
- Geht weiter in: Gemeinsamer Zustand: nach oben · Effekte · Gemeinsamer Zustand · Routing
- Aufgaben zum Thema: Zustand