Reaktiver Zustand
Auftrag
Martina Hostettler hat die Fragen vom Tresen in einen Auftrag gefasst.
Als Mitglied möchte ich den Katalog nach dem Namen durchsuchen und nach Kategorie filtern, damit ich schnell finde, was ich brauche.
Die Akzeptanzkriterien lauten:
- Ein Suchfeld zeigt nur Dinge, deren Name den Suchtext enthält, ohne auf Gross- und Kleinschreibung zu achten.
- Eine Auswahl filtert nach Kategorie und lässt sich mit der Suche verbinden.
- Über der Liste steht, wie viele Dinge gerade da sind, und unter dem Filter, wie viele Dinge gefunden wurden.
- Passt nichts, erscheint «Keine Dinge gefunden.».
Lernziele
Nach diesem Kapitel kannst du
- erklären, warum eine gewöhnliche Variable die Seite nicht aktualisiert,
- Zustand mit
refundreactivehalten und ändern, - abgeleitete Werte mit
computedberechnen, - entscheiden, wann eine Funktion und wann ein berechneter Wert passt,
- den Zustand einer Komponente in den Vue DevTools beobachten.
Zustand, dem die Seite folgt
Abschnitt betitelt „Zustand, dem die Seite folgt“In der SPA von Hand hast du nach jeder Änderung render aufgerufen. In Vue entfällt dieser Aufruf, wenn Vue von der Änderung erfährt. Dafür muss der Wert reaktiv sein. Das Beispiel zählt zurückgebrachte Dinge einmal mit einer gewöhnlichen Variablen und einmal mit ref. Überleg dir, was die beiden Zeilen nach einigen Klicks zeigen, und öffne dann die Konsole des Playgrounds.
<script setup>import { ref } from 'vue'
let returnedPlain = 0const returned = ref(0)
function countPlain() { returnedPlain++ console.log('ohne ref', returnedPlain)}
function countWithRef() { returned.value++}</script>
<template> <button type="button" @click="countPlain">Zählen ohne ref</button> <button type="button" @click="countWithRef">Zählen mit ref</button> <p>Ohne ref: {{ returnedPlain }}</p> <p>Mit ref: {{ returned }}</p></template>Ein Klick auf «Zählen ohne ref» erhöht die Variable, wie die Konsole zeigt, aber die Seite bleibt bei 0. Vue erfährt nichts von der Änderung. Ein Klick auf «Zählen mit ref» ändert einen reaktiven Wert, Vue rendert die Komponente neu und liest dabei auch die gewöhnliche Variable wieder. Darum springt die erste Zeile plötzlich auf den aktuellen Stand. Darauf darfst du dich nicht verlassen. Alles, was die Seite zeigen soll und sich ändert, gehört in einen reaktiven Wert.
ref(0) gibt ein Objekt mit der Eigenschaft value zurück. Im Script liest und schreibst du den Wert über .value, im Template packt Vue ihn selbst aus, {{ returned }} genügt. Vue merkt sich beim Rendern, welche Refs das Template liest, und rendert die Komponente neu, sobald sich einer davon ändert.
Ein Ref kann jeden Wert halten, Zahlen, Texte, Arrays und Objekte. Bei Arrays und Objekten bemerkt Vue auch Änderungen im Inneren, etwa items.value.push(item) oder item.value.name = 'Leiter'. Du kannst den ganzen Wert ersetzen, wie es die App mit items.value = data nach dem Laden tut. Die Variable selbst deklarierst du mit const, denn du änderst nie die Variable, sondern nur ihren Wert value.
reactive
Abschnitt betitelt „reactive“reactive macht ein Objekt selbst reaktiv. Seine Felder brauchen kein .value. Dahinter steckt ein Proxy, wie du ihn im Exkurs Proxy und Getter aus Teil 1 kennengelernt hast. Lies das Beispiel und überleg dir, was die letzte Zeile nach einem Klick auf «Nach «säge» suchen» zeigt.
<script setup>import { reactive } from 'vue'
// reactive makes the object itself reactive, its fields need no .valueconst filters = reactive({ query: '', onlyAvailable: false })
// Destructuring copies the current value, query no longer follows the objectconst { query } = filters
function reset() { // Change the fields, never replace the whole object filters.query = '' filters.onlyAvailable = false}</script>
<template> <button type="button" @click="filters.query = 'säge'">Nach «säge» suchen</button> <button type="button" @click="filters.onlyAvailable = !filters.onlyAvailable">Nur verfügbare</button> <button type="button" @click="reset">Zurücksetzen</button> <p>{{ filters }}</p> <p>Aus der Destrukturierung: {{ JSON.stringify(query) }}</p></template>Die letzte Zeile bleibt leer. const { query } = filters kopiert den Wert, den query in diesem Moment hat, und die Kopie folgt dem Objekt nicht. Dasselbe passiert, wenn du der Variablen ein neues Objekt zuweist. Das Template zeigt dann weiter das alte, darum ändert reset die Felder einzeln. reactive funktioniert ausserdem nur mit Objekten, nicht mit Zahlen oder Texten.
Die Dokumentation von Vue empfiehlt ref als erste Wahl, weil es für alle Werte gleich funktioniert und diese Fallen nicht kennt. Der Kurs folgt dem. reactive passt für eine Gruppe von Feldern, die zusammengehören, etwa die Felder eines Formulars.
computed
Abschnitt betitelt „computed“Viele Werte lassen sich aus anderen berechnen, die Zahl der Dinge, die da sind, oder die Liste, die zum Suchtext passt. Solche Werte speicherst du nicht in einem eigenen Ref, du leitest sie mit computed ab. Vue ruft die Funktion auf, merkt sich das Ergebnis und rechnet erst neu, wenn sich einer der Werte ändert, die die Funktion gelesen hat.
App.vue
<script setup>import { computed, ref } from 'vue'
import { items } from './items.js'
const query = ref('')const onlyAvailable = ref(false)
// Vue recalculates the list only when query or onlyAvailable changeconst visibleItems = computed(() => { const text = query.value.trim().toLowerCase() return items.filter( (item) => item.name.toLowerCase().includes(text) && (!onlyAvailable.value || item.availableFrom === null), )})
const availableCount = computed(() => items.filter((item) => item.availableFrom === null).length)</script>
<template> <p>{{ availableCount }} von {{ items.length }} Dingen sind da</p> <!-- v-model keeps the field and query in step, the chapter on forms explains it --> <input v-model="query" type="search" aria-label="Im Katalog suchen"> <button type="button" :aria-pressed="onlyAvailable" @click="onlyAvailable = !onlyAvailable">Nur verfügbare</button> <ul> <li v-for="item in visibleItems" :key="item.id">{{ item.name }}</li> </ul></template>items.js
// Twelve things from the catalog, availableFrom is null while a thing is in the shopexport const items = [ { id: 1, name: 'Bohrmaschine', availableFrom: null }, { id: 2, name: 'Zelt für vier Personen', availableFrom: '2026-11-12' }, { id: 3, name: 'Raclette-Ofen', availableFrom: null }, { id: 4, name: 'Festbankgarnitur', availableFrom: '2026-11-03' }, { id: 5, name: 'Stichsäge', availableFrom: null }, { id: 6, name: 'Beamer', availableFrom: null }, { id: 7, name: 'Leiter', availableFrom: null }, { id: 11, name: 'Akkuschrauber', availableFrom: '2026-11-05' }, { id: 15, name: 'Kreissäge', availableFrom: null }, { id: 36, name: 'Zelt für zwei Personen', availableFrom: null }, { id: 55, name: 'Partyzelt', availableFrom: '2026-11-14' }, { id: 56, name: 'Lichterkette', availableFrom: null },]Das Suchfeld ist mit v-model an das Ref query gebunden. v-model hält Feld und Ref in beide Richtungen im Takt, das Kapitel «Formulare» zeigt, was dahintersteckt. Tippt jemand ins Suchfeld, ändert sich query. Vue rechnet visibleItems neu und rendert die Liste. availableCount hängt nur von items ab und bleibt, wie es ist.
Ein berechneter Wert ist ein Ref, den du nur liest. Im Script schreibst du visibleItems.value, im Template visibleItems. Die Funktion darin soll nur rechnen und einen Wert zurückgeben. Daten laden, andere Refs ändern oder auf etwas warten gehört nicht hinein, dafür gibt es Funktionen für Events und watch aus Teil 5.
Statt computed könntest du eine Funktion schreiben und sie im Template aufrufen, {{ availableCount() }}. Das Ergebnis ist dasselbe, aber Vue führt die Funktion bei jedem Rendern aus, auch wenn sich nichts geändert hat, das sie betrifft. Für alles, was sich aus dem Zustand ergibt, ist computed die klarere Wahl. Funktionen passen für Aktionen wie toggle und für kleine Umformungen mit einem Argument wie formatDate.
Zustand in den DevTools
Abschnitt betitelt „Zustand in den DevTools“In den Vue DevTools zeigt der Bereich neben dem Baum der Komponenten den Zustand der gewählten Komponente, Refs, reaktive Objekte und berechnete Werte. Tippst du ins Suchfeld, ändern sich query und visibleItems dort mit. Den Wert eines Refs kannst du in den DevTools auch ändern, und die Seite folgt sofort. So prüfst du schnell, ob ein Fehler im Zustand oder im Template steckt.
Im Code
Abschnitt betitelt „Im Code“Arbeite mit dem Beispiel zur Suche. Lies den Code und schreib auf, was du erwartest, bevor du etwas änderst.
- Wie viele Dinge zeigt die Liste bei der Suche nach «zelt»? Und wie viele, wenn zusätzlich «Nur verfügbare» aktiv ist?
- Ergänze in
visibleItemsdie Zeileconsole.log('neu berechnet'). Wann erscheint die Meldung, und wann nicht? - Mach aus
availableCounteine gewöhnliche Funktion und passe das Template an. Was ändert sich für die Seite, und was für die Arbeit von Vue? - Ersetze
const query = ref('')durchlet query = ''. Was passiert beim Tippen, und warum?
Aufgaben
Abschnitt betitelt „Aufgaben“Die Aufgaben machen den Katalog aus dem letzten Kapitel durchsuchbar. Du findest das Projekt im Kursrepo im Ordner exercises/03-vue/reactive-state/de. Kopiere den Ordner, installiere die Pakete und starte die Tests.
npm installnpm testAufgabe Einstieg
Zeig über der Liste in einem Element mit der Klasse summary, wie viele Dinge da sind, etwa «49 von 60 Dingen sind da». Berechne die Zahl mit computed.
Lösung
const availableCount = computed(() => items.filter((item) => item.availableFrom === null).length) <p class="summary">{{ availableCount }} von {{ items.length }} Dingen sind da</p>items ändert sich in diesem Projekt nicht, darum rechnet Vue die Zahl nur einmal. Kommen die Daten wie im Fallbeispiel vom Server, rechnet derselbe Code nach dem Laden von selbst neu.
Aufgabe Vertiefung
Ergänze ein Suchfeld <input id="search" type="search"> mit einer Beschriftung und binde es mit v-model an ein Ref query. Die Liste zeigt nur Dinge, deren Name den Suchtext enthält, ohne auf Gross- und Kleinschreibung und auf Leerzeichen am Anfang und am Ende zu achten. Berechne die Liste mit computed und lass v-for über diese Liste laufen.
Lösung
const visibleItems = computed(() => { const text = query.value.trim().toLowerCase() return items.filter( (item) => item.name.toLowerCase().includes(text) && (category.value === '' || item.category === category.value), )})Die Bedingung mit category kommt erst in der Knobelei dazu. Ohne sie lautet der Filter item.name.toLowerCase().includes(text). trim entfernt die Leerzeichen am Rand, toLowerCase gleicht die Schreibweise an. Ein leerer Suchtext ist in jedem Namen enthalten, darum zeigt die Liste dann alle Dinge. Im Template läuft v-for über visibleItems statt über items.
Aufgabe Knobelei
Ergänze eine Auswahl <select id="category">. Die erste Option «Alle Kategorien» hat den Wert '', danach folgen die Kategorien aus den Daten, jede einmal und in alphabetischer Reihenfolge. Die Liste berücksichtigt Suche und Kategorie zusammen. Ein Element mit der Klasse hits nennt die Zahl der Treffer, «10 Dinge gefunden» oder «1 Ding gefunden». Passt nichts, steht statt der Liste «Keine Dinge gefunden.». new Set entfernt doppelte Werte, localeCompare vergleicht Texte für die Sortierung.
Lösung
<script setup>import { computed, ref } from 'vue'
import items from '../data/items.json'import { formatDate } from './format.js'
const query = ref('')const category = ref('')
const availableCount = computed(() => items.filter((item) => item.availableFrom === null).length)
// Every category once, in alphabetical orderconst categories = computed(() => [...new Set(items.map((item) => item.category))].sort((a, b) => a.localeCompare(b, 'de-CH')),)
const visibleItems = computed(() => { const text = query.value.trim().toLowerCase() return items.filter( (item) => item.name.toLowerCase().includes(text) && (category.value === '' || item.category === category.value), )})
const hits = computed(() => visibleItems.value.length === 1 ? '1 Ding gefunden' : `${visibleItems.value.length} Dinge gefunden`,)</script>
<template> <h1>Katalog</h1> <p class="summary">{{ availableCount }} von {{ items.length }} Dingen sind da</p> <div class="fields"> <label for="search">Suchen</label> <input id="search" v-model="query" type="search"> <label for="category">Kategorie</label> <select id="category" v-model="category"> <option value="">Alle Kategorien</option> <option v-for="name in categories" :key="name">{{ name }}</option> </select> </div> <p class="hits">{{ hits }}</p> <ul v-if="visibleItems.length > 0" class="catalog"> <li v-for="item in visibleItems" :key="item.id" :class="{ 'on-loan': item.availableFrom !== null }"> <span class="name">{{ item.name }}</span> <span class="category">{{ item.category }}</span> <span v-if="item.availableFrom === null" class="state">Verfügbar</span> <span v-else class="state">Wieder verfügbar ab {{ formatDate(item.availableFrom) }}</span> </li> </ul> <p v-else class="empty">Keine Dinge gefunden.</p></template>categories sammelt die Kategorien mit new Set und sortiert sie mit localeCompare und der Sprache der Seite, damit Umlaute richtig einsortiert werden. Eine Option ohne Attribut value liefert ihren Text als Wert, darum reicht {{ name }}. hits ist ein eigener berechneter Wert, weil er von visibleItems abhängt. Berechnete Werte dürfen andere berechnete Werte lesen.
Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
count++ im Script ändert nichts, oder es erscheint ein Fehler. | Im Script count.value++ schreiben, das Ref selbst ist ein Objekt. |
{{ count.value }} im Template zeigt nichts. | Im Template packt Vue das Ref aus, {{ count }} genügt. |
Nach const { query } = filters folgt query dem Objekt nicht mehr. | Über das Objekt zugreifen, filters.query, oder gleich ref verwenden. |
Ein neues Objekt für eine Variable mit reactive, die Seite zeigt weiter die alten Werte. | Die Felder einzeln ändern oder ein ref verwenden und .value ersetzen. |
Vue meldet Write operation failed: computed value is readonly. | Einen berechneten Wert nicht zuweisen. Die Refs ändern, aus denen er berechnet wird. |
Ein KI-Assistent schreibt data() und this.query. | Das ist die Options API. In <script setup> entsteht Zustand mit ref, und es gibt kein this. |
Ein KI-Assistent hält eine gefilterte Liste mit watch in einem eigenen Ref aktuell. | Abgeleitete Werte mit computed berechnen, das ist kürzer und kann nicht aus dem Takt geraten. |
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Warum zeigt die Seite die Änderung einer gewöhnlichen Variablen nicht an?
- Wann brauchst du
.value, und wann nicht? - Was geht verloren, wenn du ein reaktives Objekt destrukturierst?
- Worin unterscheidet sich ein berechneter Wert von einer Funktion, die das Template aufruft?
Antworten
- Vue erfährt nichts von der Änderung. Nur reaktive Werte melden Vue, dass sich etwas geändert hat.
- Im Script bei Refs und berechneten Werten. Im Template packt Vue sie aus, und bei
reactivebraucht es nie.value. - Die Verbindung. Die Variablen bekommen den Wert dieses Moments und folgen dem Objekt danach nicht mehr.
- Vue merkt sich das Ergebnis eines berechneten Werts und rechnet nur neu, wenn sich seine Abhängigkeiten ändern. Eine Funktion läuft bei jedem Rendern.
Auftrag
Suche und Filter kommen am Tresen gut an. Jetzt fehlt Martina Hostettler noch das Wichtigste. «Die Mitglieder sollen ein Ding reservieren können, mit dem ersten und dem letzten Tag, und die App soll Fehler gleich melden.» Das nächste Kapitel baut das Formular dafür.
Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.