Zum Inhalt
wannacode

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 als Ereignis (emit) 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.

Vue

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

  1. App hält items und berechnet count.
  2. Daten kommen als Props nach unten: count zu CartBadge, items zu ProductList.
  3. Änderungen gehen als Nachricht nach oben: Der Button in ProductCard ändert den Warenkorb nicht, er sagt App: „Füge dieses Produkt hinzu“.
<script setup>
const items = ref([]);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
function add(product) { /* ändert items */ }
</script>
<template>
<header><CartBadge :count="count" /></header>
<main><ProductList :products="PRODUCTS" :items="items" @add="add" /></main>
</template>

Das Kind deklariert mit defineEmits(['add']), welche Ereignisse es sendet, und sendet sie mit emit('add', product). Das Elternelement hört mit @add="add" zu, genauso wie es bei einem Button @click abhört.

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.

App.vue
<script setup>
const items = ref([]);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0),
);
function add(product) {
plusOne(items.value, product);
}
</script>
<template>
<CartBadge :count="count" />
<ProductList :items="items" @add="add" />
</template>
<!-- ProductList.vue -->
<ProductCard v-for="p in PRODUCTS" :key="p.id" :product="p"
:qty="qtyOf(items, p)" @add="(product) => emit('add', product)" />
<!-- ProductCard.vue -->
<button @click="emit('add', product)">…</button>
Schritt für Schritt, in Worten
  1. CartBadge und ProductList sind Geschwister: Beide sind Kinder von App. Zuerst liegt der Warenkorb in ProductList, und CartBadge hat eine eigene Zahl.
  2. 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.
  3. Die Lösung: Zieh den Warenkorb zum nächsten gemeinsamen Elternelement der beiden, App. Der Zähler gibt seine eigene Kopie auf.
  4. 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.
  5. Das Kind besitzt den Warenkorb nicht, also kann es ihn nicht ändern. Der Klick sendet das Ereignis add mit dem Produkt. ProductList reicht es weiter, und App hört mit @add zu und ruft add() auf.
  6. 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.
  7. 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.

Vue

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.

Das Ereignis ist ein Verb ohne on: add, remove, change. Das Kind deklariert es in defineEmits(['add']), sendet es mit emit('add', product), und das Elternelement hört mit @add zu. Das zweite Argument von emit ist das, was die Funktion des Elternelements bekommt.

Probier es aus

Fülle die Lücke

Was gehört in die Lücke, damit der Klick das Elternelement erreicht?

<script setup>
defineProps(['product']);
const emit = defineEmits(['add']);
</script>

<template>
  <button @click="('add', product)">Hinzufügen</button>
</template>

Probier es aus

Finde den Fehler

Ein Klick auf „Hinzufügen“ tut nichts, und die Konsole ist leer. Welche Zeile ist schuld?

Wähle die Zeile mit dem Fehler.

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: provide/inject und Pinia. 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. props.qty = 3 ändert in Vue nichts und schreibt [Vue warn] Set operation on key "qty" failed: target is readonly in die Konsole. Ein Kind, das etwas ändern will, meldet nach oben.

v-model auf einer Komponente ist dasselbe. <QtyInput v-model="qty" /> ist die Kurzform von :modelValue="qty" und @update:modelValue="(v) => qty = v". Die Daten kommen weiterhin als Prop nach unten und gehen weiterhin als Ereignis nach oben.

Wozu defineEmits. Deklarierte Ereignisse sind Dokumentation: Du siehst, was die Komponente sagt. Und Vue prüft sie: Sendet die Karte emit('added') und hat nur add deklariert, meldet die Konsole Component emitted event "added" but it is neither declared in the emits option….

Eine Kopie ist kein Hochziehen. ref(props.items) im Kind kopiert das Array nicht: Es ist dasselbe reaktive Array des Elternelements, und alles, was hinzukommt, erscheint. Aber items.value = items.value.filter(…) ändert nur die Box des Kindes. Das Entfernen erreicht das Elternelement nie, und die beiden Warenkörbe laufen auseinander.

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

Vue

Test

Teste dich

1. CartBadge steht im Header, ProductList im Hauptteil. Beide sind Kinder von App. Wo soll der Warenkorb liegen?
2. ProductCard macht emit('add', product), und App schreibt <ProductList :items="items" /> ohne @add. Was passiert?
3. Warum ändert ProductCard den Warenkorb nicht selbst, sondern meldet nach oben?
4. Den Warenkorb braucht eine Komponente fünf Ebenen unter App. Die drei Komponenten dazwischen reichen ihn nur nach unten weiter. Wie heißt das?

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