<v/>
{ }
Vue Reactivity · computed · watch · watchEffect · Composition API
watch vs watchEffect
vs computed – wann was?

computed, watch und watchEffect sind die drei Reaktivitäts-Primitive der Vue 3 Composition API – und alle drei lösen verschiedene Probleme. Wer sie verwechselt, schreibt entweder unnötige Seiteneffekte, verzichtet auf Caching oder debuggt endlose Watcher-Kaskaden, die ein computed in einer Zeile gelöst hätte.

13 Min. Lesezeit computed · watch · watchEffect · immediate · deep · onCleanup Vue 3 · Composition API · TypeScript

1. Das Reaktivitätssystem von Vue 3 verstehen

Vue 3 baut sein Reaktivitätssystem auf dem Proxy-Objekt von JavaScript auf. Wenn ein reaktiver Wert (via ref(), reactive() oder shallowRef()) innerhalb eines reaktiven Kontexts gelesen wird, registriert Vue automatisch eine Abhängigkeit. Ändert sich der Wert, benachrichtigt Vue alle registrierten Abhängigkeiten und triggert deren Neuberechnung oder Ausführung. Dieser Mechanismus läuft transparent im Hintergrund – Entwickler müssen Abhängigkeiten nicht manuell deklarieren oder beobachtungslisten verwalten.

computed, watch und watchEffect sind die drei Hauptwege, auf Änderungen reaktiver Daten zu reagieren. Sie unterscheiden sich fundamental in ihrem Zweck: computed berechnet einen neuen, abgeleiteten Wert aus reaktiven Quellen und cached das Ergebnis. watch beobachtet explizit definierte reaktive Quellen und führt bei Änderungen Seiteneffekte aus, mit Zugriff auf alten und neuen Wert. watchEffect führt automatisches Dependency-Tracking durch und läuft sofort beim ersten Aufruf – wie ein computed, der einen Seiteneffekt statt einen Rückgabewert produziert. Die Wahl des falschen Primitives führt zu suboptimalem Code, der entweder zu viel berechnet, zu wenig cached oder unkontrollierte Seiteneffekte erzeugt.

2. computed: abgeleitete Werte mit Caching

computed in Vue 3 ist der richtige Weg für jeden Wert, der sich aus reaktiven Quellen ableitet und im Template oder in anderem Code konsumiert wird. Der entscheidende Vorteil gegenüber einer normalen Funktion: Caching. Eine computed-Property wird nur dann neu berechnet, wenn sich eine ihrer Abhängigkeiten geändert hat. Wird sie mehrfach in einem Template referenziert, gibt Vue den gecachten Wert zurück, ohne die Getter-Funktion erneut auszuführen. Eine normale Methode, die in einem Template aufgerufen wird, wird bei jedem Render-Durchlauf neu ausgeführt – unabhängig davon, ob sich die Eingabedaten geändert haben.

Ein häufiger Fehler: Entwickler verwenden watch, um einen abgeleiteten Wert in einem separaten ref() zu halten. Beispiel: Ein Watcher beobachtet ein Array und setzt bei Änderung filteredItems.value = items.value.filter(...). Das ist unnötiger Code, der zudem das Caching von computed nicht nutzt und bei jeder Array-Änderung sofort ausgeführt wird – auch wenn der Wert im Template gerade nicht dargestellt wird. Das äquivalente computed berechnet nur dann, wenn filteredItems wirklich gelesen wird. Für schreibbare abgeleitete Werte bietet computed ein Getter-Setter-Paar – nützlich für bidirektionale v-model-Bindings auf berechneten Werten.


// Comparing computed vs watch for derived values
import { ref, computed, watch } from 'vue'

// WRONG: watch to maintain a derived ref — misuse of watch
const items = ref([{ id: 1, active: true }, { id: 2, active: false }])
const activeItems = ref([])
watch(items, (newItems) => {
  activeItems.value = newItems.filter(i => i.active)
}, { immediate: true, deep: true })
// Problem: executes even when activeItems is not consumed; no caching

// RIGHT: computed — cached, lazy, no side effects
const activeItems2 = computed(() =>
  items.value.filter(i => i.active)
)
// Only recalculated when items.value changes AND activeItems2.value is read

// Writable computed for bidirectional v-model
const firstName = ref('Maria')
const lastName = ref('Muster')

const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value: string) => {
    const [first, ...rest] = value.split(' ')
    firstName.value = first
    lastName.value = rest.join(' ')
  }
})
// <input v-model="fullName" /> — reads and writes via computed

3. watch: Seiteneffekte mit Kontrolle

watch in Vue 3 ist der richtige Weg für Seiteneffekte, die auf Änderungen reaktiver Quellen reagieren und Zugriff auf den alten Wert benötigen. Das sind die Fälle, die computed nicht abdecken kann: API-Calls beim Ändern einer ID, das Synchronisieren von State mit localStorage, das Auslösen von Animationen oder das Protokollieren von Änderungen für Analytics. watch ist explizit: Die beobachteten Quellen werden als erstes Argument definiert, nicht durch automatisches Dependency-Tracking während der Ausführung des Callbacks.

Die Option immediate: true lässt den Watcher sofort beim Erstellen ausführen – nützlich, wenn der Seiteneffekt auch für den initialen Wert gelten soll, ohne den Code doppelt zu schreiben. Die Option deep: true beobachtet verschachtelte Objekte und Arrays tief – aber Vorsicht: Deep-Watching großer Objekte ist teuer, weil Vue alle Properties rekursiv traversieren muss. Für reactive()-Objekte läuft der Watcher standardmäßig als Deep-Watcher; für ref()-wrapped Primitive ist kein deep: true nötig. watch gibt außerdem die Stop-Funktion zurück, mit der der Watcher manuell deregistriert werden kann.

4. watchEffect: automatisches Dependency-Tracking

watchEffect in Vue 3 kombiniert die sofortige Ausführung von immediate: true mit automatischem Dependency-Tracking – wie ein computed, der statt eines Rückgabewerts einen Seiteneffekt produziert. Der Effekt wird sofort beim ersten Aufruf ausgeführt und alle reaktiven Werte, die dabei gelesen werden, werden als Abhängigkeiten registriert. Ändert sich eine dieser Abhängigkeiten, wird der Effekt erneut ausgeführt. Das macht watchEffect kompakter als watch für Fälle, in denen mehrere reaktive Quellen beobachtet werden sollen und kein Zugriff auf den alten Wert benötigt wird.

Der wichtigste Unterschied zu watch: watchEffect liefert keinen alten Wert. Es ist nicht möglich, im Callback auf den vorherigen Zustand einer reaktiven Quelle zuzugreifen. Außerdem ist die Dependency-Liste implizit – was im Effekt gelesen wird, wird beobachtet. Das kann zu überraschenden Nebeneffekten führen, wenn im Effekt-Code reaktive Werte gelesen werden, die eigentlich nicht als Trigger dienen sollen. In solchen Fällen ist watch mit expliziter Quellenangabe die klarere Wahl. watchEffect eignet sich besonders gut für DOM-Synchronisation, Logging und andere einfache Effekte, bei denen alle gelesenen reaktiven Werte auch als Trigger agieren sollen.


// watch vs watchEffect — key differences demonstrated
import { ref, watch, watchEffect } from 'vue'

const userId = ref(1)
const section = ref('profile')

// watch: explicit sources, access to old value, NOT immediate by default
watch(userId, async (newId, oldId) => {
  console.log(`User changed from ${oldId} to ${newId}`)
  // oldId is available — not possible with watchEffect
  await fetchUser(newId)
})

// watchEffect: auto-tracks dependencies, immediate, no old value
// Both userId AND section are tracked — any change triggers refetch
watchEffect(async () => {
  // Both refs are read here → both are dependencies
  const data = await fetchUserSection(userId.value, section.value)
  userData.value = data
})
// Equivalent watch would need: watch([userId, section], ...)

// watchEffect with cleanup — for cancellable async operations
watchEffect(async (onCleanup) => {
  const controller = new AbortController()

  // Register cleanup before async call
  onCleanup(() => controller.abort())

  const data = await fetch(`/api/users/${userId.value}`, {
    signal: controller.signal
  }).then(r => r.json())

  userData.value = data
})
// If userId changes before fetch completes → previous request is aborted

5. Die wesentlichen Unterschiede im Detail

Der tiefste Unterschied zwischen computed, watch und watchEffect liegt in ihrer Ausführungszeit und ihrem Zweck. computed ist lazy: Der Getter wird erst ausgeführt, wenn der computed-Wert gelesen wird. Wenn niemand den Wert konsumiert, berechnet Vue ihn auch dann nicht, wenn sich Abhängigkeiten ändern. Das ist der fundamentale Performance-Vorteil gegenüber einem Watcher mit sofortiger Ausführung.

watch ist standardmäßig lazy in Bezug auf den ersten Aufruf (kein immediate per default), aber der Callback wird geplant und ausgeführt, sobald sich eine Quelle ändert – unabhängig davon, ob das Ergebnis irgendwo konsumiert wird. watchEffect ist sofort und führt den Effekt synchron vor dem ersten Render aus (oder asynchron mit flush: 'post' für DOM-Zugriffe). Die Flush-Option steuert, wann genau der Watcher relativ zu Vue's Render-Zyklus ausgeführt wird: pre (vor dem Render, Standard), post (nach dem Render, für DOM-Zugriffe) und sync (synchron bei jeder Änderung, selten sinnvoll).

6. Cleanup und Stoppen von Watchern

Watcher müssen aufgeräumt werden, wenn ihre Komponente destroyed wird. In der Composition API werden Watcher, die innerhalb von setup() oder <script setup> erstellt werden, automatisch gestoppt, wenn die Komponente unmounted wird. Das gilt für alle drei Primitives: computed, watch und watchEffect. Wer einen Watcher außerhalb des Komponentenlebenszyklus erstellt – z. B. in einem asynchronen Callback nach einem await – muss die zurückgegebene Stop-Funktion manuell aufrufen, weil Vue die Verbindung zur Komponente in diesem Fall nicht kennt.

Für asynchrone Operationen innerhalb von watchEffect und watch ist die onCleanup-Funktion das richtige Werkzeug. Sie wird vor dem nächsten Ausführen des Effekts und beim Stoppen des Watchers aufgerufen. Das Standardmuster für abbrechbare API-Calls: einen AbortController erstellen, in onCleanup registrieren und das AbortSignal an den Fetch-Call übergeben. Wenn die Quelle sich ändert, bevor der Call abgeschlossen ist, wird der AbortController aktiviert und der laufende Request abgebrochen. Ohne dieses Muster stapeln sich ausstehende Requests und könnten State in unerwarteter Reihenfolge setzen.

7. API-Calls mit watch und watchEffect

Das Triggern von API-Calls bei Änderungen reaktiver Werte ist einer der häufigsten Anwendungsfälle für watch und watchEffect in Vue 3. Ein Produktlisten-Filter, der sich bei Route-Param-Änderungen aktualisiert, ein Suchfeld mit Debounce, das bei Eingabe neue Ergebnisse lädt, oder ein Detail-Bereich, der sich bei geänderter ID neu befüllt – all das sind Seiteneffekte, die auf reaktive Änderungen reagieren und daher in watch oder watchEffect gehören, nicht in computed.

Für Suchfelder mit Debouncing ist watch mit { debounce } aus der VueUse-Bibliothek (via watchDebounced) oder ein manuell implementiertes Debounce-Pattern der richtige Weg. Das rohe watch führt den Callback bei jeder Änderung sofort aus – bei einem Suchfeld bedeutet das einen API-Call pro getipptem Zeichen. Das manuell mit einem Debounce-Wrapper zu kombinieren ist die korrekte Lösung. VueUse bietet mit watchDebounced und watchThrottled praktische Abstraktionen, die das saubere Debounce/Throttle-Verhalten für Watcher kapseln, ohne manuell mit setTimeout und Cleanup hantieren zu müssen.


// Practical patterns: watch and watchEffect for API calls
import { ref, watch } from 'vue'
import { watchDebounced } from '@vueuse/core'

const searchQuery = ref('')
const results = ref([])
const isLoading = ref(false)
const error = ref<Error | null>(null)

// Debounced search: wait 300ms after last keystroke before fetching
watchDebounced(
  searchQuery,
  async (query) => {
    if (!query.trim()) {
      results.value = []
      return
    }

    isLoading.value = true
    error.value = null

    try {
      const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      results.value = await response.json()
    } catch (e) {
      error.value = e as Error
      results.value = []
    } finally {
      isLoading.value = false
    }
  },
  { debounce: 300, maxWait: 1000 }
)

// watch with old value: only reload if id actually changed (not on filter change)
const productId = ref(42)
const filterParams = ref({ sortBy: 'price', order: 'asc' })

watch(productId, async (newId, oldId) => {
  // Skip if id hasn't changed — e.g. after route re-navigation to same product
  if (newId === oldId) return
  await loadProduct(newId)
}, { immediate: true })

8. Häufige Fehlerquellen und wie man sie vermeidet

Die häufigste Fehlerquelle bei computed: Seiteneffekte im Getter. computed-Getter sollen pure Funktionen sein – kein State schreiben, keine API-Calls, keine DOM-Manipulation. Vue kann Getter mehrfach ausführen, in Server-Side-Rendering-Umgebungen können sie synchron ausgeführt werden, und asynchrone computeds werden nicht unterstützt. Wer einen asynchronen Wert in einer computed-ähnlichen Struktur braucht, kann asyncComputed aus VueUse nutzen oder einen eigenen Composable mit ref und watch/watchEffect bauen.

Bei watch ist der häufigste Fehler das versehentliche Beobachten des Wertes statt der Referenz. watch(myRef.value, ...) beobachtet den Wert zum Zeitpunkt des Aufrufs, nicht reaktiv das Ref. Korrekt ist watch(myRef, ...) oder watch(() => myObj.property, ...) für Properties auf reaktiven Objekten. Bei Objekt-Quellen: watch(() => ({ ...myReactive })) mit einem Getter, der ein neues Objekt zurückgibt, löst den Callback bei jeder Property-Änderung aus, weil immer eine neue Referenz entsteht. Das ist oft nicht gewollt – deep: true auf der direkten Quelle ist in diesem Fall die richtige Lösung.

9. Entscheidungstabelle: computed, watch oder watchEffect?

Die Entscheidung zwischen computed, watch und watchEffect folgt einem klaren Muster, das sich in einer Tabelle zusammenfassen lässt:

Kriterium computed watch watchEffect
Zweck Abgeleiteter Wert Seiteneffekt bei Änderung Sofort-Effekt, auto-tracking
Gibt zurück Reaktiven Wert Stop-Funktion Stop-Funktion
Alter Wert Nicht verfügbar Verfügbar als 2. Param Nicht verfügbar
Läuft sofort? Lazy (nur wenn gelesen) Nein (opt-in: immediate) Ja, immer
Dependency-Tracking Automatisch Explizit (kontrolliert) Automatisch
Async? Nicht unterstützt Ja, mit onCleanup Ja, mit onCleanup

Die Entscheidungsregel in einem Satz: Brauche ich einen Wert aus reaktiven Quellen? Dann computed. Brauche ich einen Seiteneffekt und den alten Wert oder explizite Quellenkontrolle? Dann watch. Brauche ich einen sofortigen Seiteneffekt auf mehreren reaktiven Quellen, ohne Zugriff auf alte Werte? Dann watchEffect. Diese drei Fragen lösen 95% aller Entscheidungssituationen bei der täglichen Vue-3-Entwicklung.

Mironsoft

Vue.js-Entwicklung, Composition API und Reaktivitätsarchitektur

Vue 3 Reaktivität richtig einsetzen?

Wir analysieren bestehende Vue-Anwendungen auf Reaktivitäts-Antipatterns, unnötige Watcher und fehlende computed-Optimierungen – und implementieren saubere, performante Lösungen.

Code-Review

Analyse von watch/watchEffect-Missbrauch, fehlenden computeds und Cleanup-Problemen

Performance-Audit

Identifizieren unnötiger Re-Renders durch falsch gesetzte Reaktivitäts-Primitives

Composable-Design

Saubere Composables mit korrektem Cleanup, Dependency-Management und TypeScript-Typen

10. Zusammenfassung

computed, watch und watchEffect sind die drei Reaktivitäts-Primitive der Vue 3 Composition API, die verschiedene Probleme lösen. computed erzeugt gecachte, abgeleitete Werte aus reaktiven Quellen – immer wenn ein Wert gebraucht wird, kein Seiteneffekt nötig ist und Performance durch Caching profitiert. watch beobachtet explizit definierte Quellen, liefert alten und neuen Wert und eignet sich für kontrollierte Seiteneffekte wie API-Calls, localStorage-Sync und Analytics. watchEffect führt sofortige, automatisch getrackte Effekte aus, ohne Zugriff auf alte Werte, und ist die kompakteste Wahl für synchrone Synchronisationsaufgaben.

Die häufigsten Fehler: Seiteneffekte in computed-Gettern, watch zum Halten eines abgeleiteten Werts statt computed, fehlendes Cleanup für asynchrone Operationen und Deep-Watching großer Objekte ohne Notwendigkeit. Wer die drei Primitives konsequent nach ihrem Zweck einsetzt, schreibt reaktiven Vue-Code, der performant, testbar und für das gesamte Team nachvollziehbar ist.

watch vs watchEffect vs computed — Das Wichtigste auf einen Blick

computed

Abgeleiteter, gecachter Wert aus reaktiven Quellen. Lazy – wird nur berechnet, wenn gelesen. Keine Seiteneffekte, kein async. Erste Wahl für Template-Werte.

watch

Explizite Quellenangabe, Zugriff auf alten Wert. Ideal für API-Calls, localStorage-Sync und alle Seiteneffekte, bei denen der Vorher-/Nachher-Vergleich relevant ist.

watchEffect

Sofort, automatisches Dependency-Tracking. Ideal für synchrone Synchronisationsaufgaben mit mehreren Quellen. Kein Zugriff auf alten Wert.

Cleanup

onCleanup in watch/watchEffect für abbrechbare Async-Operationen – AbortController-Muster verhindert Race Conditions und gestapelte Requests.

11. FAQ: watch vs watchEffect vs computed in Vue 3

1computed vs watch – Hauptunterschied?
computed erzeugt gecachte Werte, lazy – nur berechnet wenn gelesen. watch führt Seiteneffekte aus und liefert alten + neuen Wert. computed für Werte, watch für Aktionen.
2watchEffect statt watch – wann?
Wenn der Effekt sofort laufen soll, mehrere Quellen automatisch getrackt werden und kein Zugriff auf den alten Wert nötig ist. watch für explizite Quellenkontrolle und Vorher-/Nachher-Vergleich.
3Kann computed asynchron sein?
Nein. Getter müssen synchron sein. Für async abgeleitete Werte: asyncComputed aus VueUse oder ref + watchEffect kombinieren.
4Was macht onCleanup?
Registriert eine Funktion, die vor der nächsten Watcher-Ausführung aufgerufen wird. AbortController-Muster: vor dem Fetch registrieren, verhindert Race Conditions und gestapelte Requests.
5watch zum Halten eines abgeleiteten Werts?
Antipattern. computed ist lazy und cached – wird nur berechnet wenn gelesen. watch führt bei jeder Änderung aus, egal ob der Wert konsumiert wird.
6Property auf reaktivem Objekt beobachten?
watch(() => myReactive.property, callback) – mit Getter-Funktion. Direkt watch(myReactive.property, ...) beobachtet den Wert zum Zeitpunkt des Aufrufs, nicht reaktiv.
7Watcher manuell stoppen?
Innerhalb setup() stoppen Watcher automatisch mit der Komponente. In async Callbacks oder außerhalb: const stop = watch(...); stop() manuell aufrufen.
8flush: 'pre' vs 'post'?
pre (Standard): vor dem DOM-Update. post: nach DOM-Update, für Template-Ref-Zugriffe auf aktualisiertes DOM. sync: synchron, selten sinnvoll.
9watch-Callback debounce?
watchDebounced aus VueUse mit { debounce: 300 }. Manuell: debounced Wrapper im Callback und Timeout in onCleanup clearen.
10Mehrere Quellen mit einem watch?
watch([refA, refB], ([newA, newB], [oldA, oldB]) => ...) – Arrays als Quellen und im Callback. Alternativ watchEffect für automatisches Tracking aller gelesenen Refs.