Zum Inhalt
wannacode

Abgeleiteter Zustand

Kurz gesagt: Abgeleiteter Zustand ist ein Wert, der aus dem Zustand berechnet wird (eine Anzahl, eine Summe, eine gefilterte Liste), statt getrennt gespeichert zu werden; computed behält das Ergebnis bis zur nächsten Änderung.

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.

Vue

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 = ref(START);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));

<script setup> läuft einmal, wenn die Komponente erzeugt wird. Deshalb steht die Berechnung in einer Funktion, die an computed übergeben wird. Vue merkt sich, dass sie items liest, und führt sie erneut aus, wenn sich items ändern. Im Template schreibst du {{ total }}, ohne .value.

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.

<script setup>
const items = ref(START);
const savedTotal = ref(26.9);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0),
);
const total = computed(() =>
items.value.reduce((s, i) => s + i.price * i.qty, 0),
);
const free = computed(() => total.value >= 30);
function more(item) {
item.qty++;
savedTotal.value += item.price;
}
function remove(item) {
items.value = items.value.filter((i) => i.id !== item.id);
}
</script>
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: berechnete Werte (computed). Vue merkt sich, dass sie items lesen, und berechnet sie neu, wenn sich items ändern: 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.

Vue

Probier es aus

Fülle die Lücke

Die Anzahl wird aus dem Warenkorb berechnet. Was gehört in die Lücke?

const items = ref(START);
const count = (() => items.value.reduce((sum, i) => sum + i.qty, 0));

Die Idee

Wann man das Ergebnis behält

computed behält das Ergebnis schon. Es wird berechnet, wenn du es zum ersten Mal liest. Danach liefert es den gemerkten Wert, solange sich die Daten, die es gelesen hat, nicht ändern. Zwei Stellen im Template mit {{ total }} berechnen die Summe also einmal.

const sorted = computed(() => products.value.toSorted(byPrice));

Eine Funktion, die im Template aufgerufen wird ({{ totalOf(items) }}), liefert auch die richtige Summe. Der Unterschied: Sie läuft bei jedem Rendern der Komponente, computed nur, wenn sich seine Abhängigkeiten ändern. Die Funktion in computed liest nur und gibt zurück. Sie ändert keinen Zustand und ruft kein fetch auf.

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 = ref('all');
const visible = computed(() =>
category.value === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category.value),
);

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

Vue

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:

Vue

Klick auf „20 % Rabatt“. Der Button ist gedrückt, und das Brot kostet immer noch 1,20 €.

const price = ref(props.product.price) kopiert die Zahl einmal, wenn <script setup> läuft. Danach ist props.product neu, aber price ist eine eigene Box, die niemand ändert.

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 mit computed(() => props.product.price * …).

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.

Ein beschreibbares computed. computed({ get, set }) liefert einen Wert, dem man auch etwas zuweisen kann: set macht aus dem neuen Wert eine Änderung des echten Zustands. Es wird selten gebraucht, zum Beispiel für ein Feld, das an einen Teil eines Objekts gebunden ist.

computed ist faul. Liest niemand total, läuft die Berechnung nicht. Deshalb gibt es darin keine Nebenwirkungen: Du weißt nicht, wann und wie oft sie läuft. Für eine Aktion bei einer Änderung gibt es watch, das nächste Modul.

Die Options API. Im älteren Stil stehen die berechneten Werte im Objekt computed: { total() { … } }. Die Idee ist dieselbe.

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.

Vue

Test

Teste dich

1. Der Warenkorb hält items. Was davon sollte ein eigener Zustand sein?
2. Was ist der Unterschied zwischen computed und einer Funktion, die im Template aufgerufen wird?
3. const price = ref(props.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 ·