Zum Inhalt
wannacode

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.

React

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
  1. Links ist der Zustand: die Warenkorb-Einträge items. Darunter ist ein zweiter Zustand, savedTotal: dieselbe Summe, als Kopie gehalten.
  2. 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 €.
  3. 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.
  4. 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.
  5. Die Kopie sagt 34,80 €, der Warenkorb ist 3,20 €. Kein Fehler, nur eine falsche Zahl: Der Zustand und seine Kopie sind auseinandergelaufen.
  6. 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.

React

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);

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“.

React

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:

React

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.

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.

React

Test

Teste dich

1. Der Warenkorb hält items. Was davon sollte ein eigener Zustand sein?
2. Wann solltest du eine Berechnung in useMemo packen?
3. const [price] = useState(product.price); in ProductCard. Das Elternelement übergibt einen neuen Preis. Was zeigt die Karte?
4. Die Liste wird nach Kategorie gefiltert. Was gehört in den Zustand?

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