Zustand
Kurz gesagt: Zustand ist ein Wert, den sich eine Komponente zwischen zwei Renderings merkt; in Vue wird er mit ref gehalten, und jede Änderung aktualisiert die Komponente auf der Seite.
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.
<script setup> läuft einmal für jede Karte, und das Template führt Vue aus. Vue führt das Template nur dann erneut aus, wenn sich etwas ändert, das es beobachtet. Eine einfache let-Variable beobachtet Vue nicht. Der Klick ändert count, aber das Template läuft nicht erneut, also bleibt die Karte bei 0.
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
<script setup>import { ref } from 'vue';
const count = ref(0);
function add() { count.value++;}</script>
<template> <p>Im Warenkorb: {{ count }}</p></template>ref(0)gibt eine Box zurück, und die Zahl steckt incount.value. Im Skript schreibst du.value.- Im Template packt Vue die Box aus: Du schreibst
{{ count }}, ohne.value. - Liest das Template
count, merkt sich Vue das. Ändert sichcount.value, führt Vue das Template erneut aus.
Den Klick-Handler gibst du als Funktion an: @click="add". In Vue funktionieren auch @click="add()" oder @click="count++": Das Template packt sie in eine Funktion, die beim Klick läuft.
Konsole
Sieh es dir an
Sieh, was bei einem Klick passiert
<script setup>const count = ref(0);
function addThree() { count.value++; count.value++; count.value++;}</script>
<template> <article> <button @click="addThree">+3</button> <p>Im Warenkorb: {{ count }}</p> </article></template>Schritt für Schritt, in Worten
- Die Karte hält count in einer Ref: einer Box, deren Wert count.value ist. Vue beobachtet, wer sie liest.
- Vue führt das Template aus. Es liest count und gibt eine Beschreibung zurück: einen Button und einen Text mit 0. Vue merkt sich, dass das Template count liest.
- 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 count.value++. Die Box ändert sich sofort: 1, 2, 3. Vue plant, das Template erneut auszuführen, aber nur einmal.
- Der Handler ist fertig. Vue führt das Template einmal aus, mit count = 3, und es gibt eine neue Beschreibung zurück.
- Vue 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.
- Der Zähler zeigt 3. Die Änderung einer Ref ist sofort da, und das Zeichnen passiert einmal, nach dem Handler.
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.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(0);
function add() {
count.value++;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<button type="button" @click="add" :aria-label="`Hinzufügen ${product.name}`">
Hinzufügen
</button>
<p>Im Warenkorb: {{ count }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const product = { id: 1, name: 'Weißbrot', price: 1.2 };
</script>
<template>
<main>
<h1>Der Shop</h1>
<ProductCard :product="product" />
</main>
</template>
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 = ref(0), mit import { ref } from 'vue', und in add schreibst du count.value++. Das Template behält {{ count }}: Mit {{ count.value }} ist der Text leer, weil count dort schon die Zahl ist.Die Idee
Die Box und die Zahl
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.value ist immer die aktuelle Zahl, also ergibt sechsmal count.value++ den Wert 6. Das Template läuft dafür nicht sechsmal. Vue sammelt die Änderungen und führt es einmal aus, nach dem Handler.
Der Fehler ist hier ein anderer. Die Box ist count, und count.value ist eine Zahl. Eine Zahl, die in eine andere Variable gelegt wird, ist eine Kopie:
let bottles = count.value;bottles += 6; // ändert die Kopie, count bleibt gleichVue beobachtet die Box, nicht jede Variable, in der die Zahl schon war. Schreib in die Box: count.value += 6.
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: Sie ändert bottles, eine Kopie der Zahl. Die Box count wird nicht angefasst, und das Template läuft nicht. Es müsste count.value += 6 heißen.
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.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(0);
function add() {
count.value++;
}
function addBox() {
count.value += 6;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<button type="button" @click="add" :aria-label="`Hinzufügen ${product.name}`">
Hinzufügen
</button>
<button type="button" @click="addBox">Packung hinzufügen (6 Stk.)</button>
<p>Im Warenkorb: {{ count }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const product = { id: 15, name: 'Mineralwasser', price: 0.9 };
</script>
<template>
<main>
<h1>Der Shop</h1>
<ProductCard :product="product" />
</main>
</template>
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; }
count.value += 6 ändert die Box, und das Template läuft erneut. let n = count.value; n += 6 ändert eine Kopie, und die Karte bewegt sich nicht. count.value = 6 addiert nicht, es setzt 6, und nach „Hinzufügen“ ist der Zähler 6, nicht 7.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
ref und reactive. reactive({ count: 0 }) macht das Objekt beobachtet und hat kein .value, funktioniert aber nur mit Objekten, und das ganze Objekt lässt sich nicht ersetzen. ref funktioniert mit jedem Wert, deshalb empfiehlt die Vue-Dokumentation es als Standard.
Wann das Template läuft. Vue führt das Template im nächsten Microtask erneut aus, wenn der Code, der gerade läuft, fertig ist. await nextTick() wartet auf dieses Rendering, zum Beispiel um das neue DOM zu lesen.
Auspacken. Im Template werden Refs auf der obersten Ebene von <script setup> ausgepackt. Eine Ref in einem einfachen Objekt ({ total: ref(0) }) wird in einem Ausdruck wie {{ cart.total + 1 }} nicht ausgepackt.
Options API. Älterer Vue-Code schreibt die Komponente als Objekt mit data(), methods und this.count. Neuer Code nutzt <script setup> und die Composition API.
Ü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.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(1);
function less() {
count.value--;
}
function more() {
count.value++;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<p>
<button type="button" @click="less" :disabled="count === 1" :aria-label="`Weniger ${product.name}`">−</button>
<button type="button" @click="more" :aria-label="`Mehr ${product.name}`">+</button>
</p>
<p>{{ count }} × {{ money(product.price) }} = {{ money(count * product.price) }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
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 },
];
</script>
<template>
<main>
<h1>Der Shop</h1>
<ProductCard v-for="p in products" :key="p.id" :product="p" />
</main>
</template>
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.vue: const count = ref(1), less macht count.value--, und more macht count.value++. „−“ ist :disabled="count === 1": So kann die Menge nicht unter 1 fallen. Die Summe ist money(count * product.price) direkt im Template: Sie ergibt sich aus count und ist kein eigener Zustand, darum geht es in der Lektion über abgeleiteten Zustand. Steht die ref in einem separaten normalen <script>, ist sie für alle Karten dieselbe, und „+“ beim Brot ändert auch den Käse.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