<v/>
{ }
Vue 3 · Reaktivität · ref · reactive · computed · watch
Vue 3 Reaktivität wirklich verstehen
ref, reactive, computed und watch von innen

Vue 3 Reaktivität ist keine Magie — sie ist ein präzises System aus JavaScript Proxy, Dependency-Tracking und Scheduling. Wer versteht, wie ref(), reactive(), computed() und watch() intern arbeiten, macht keine Reaktivitätsfehler mehr, wählt das richtige Tool für jeden Anwendungsfall und weiß sofort, warum eine Komponente sich nicht neu rendert. Dieser Artikel erklärt das Reaktivitätssystem vollständig und praxisnah.

20 Min. Lesezeit ref · reactive · computed · watch · watchEffect · Proxy Vue 3.4+ · Composition API · JavaScript

1. JavaScript Proxy: die technische Grundlage von Vue Reaktivität

Das Vue 3 Reaktivitätssystem basiert auf JavaScript Proxy — einem 2015 in ECMAScript 6 eingeführten Feature, das es erlaubt, einen Stellvertreter für ein Objekt zu erstellen, der Operationen auf diesem Objekt abfangen und modifizieren kann. Wenn Vue ein Objekt mit reactive() wrapping, erstellt es einen Proxy, der jedes Lesen (get) und Schreiben (set) von Properties abfängt. Beim Lesen registriert Vue, wer gerade liest — die aktive Effekt-Funktion. Beim Schreiben benachrichtigt Vue alle registrierten Leser, dass sich der Wert geändert hat. Das ist der Kern des Reaktivitätssystems.

Vue 2 nutzte Object.defineProperty() statt Proxy. Das führte zu bekannten Einschränkungen: Neue Properties an bestehende Objekte konnten nicht reaktiv gemacht werden (Vue.set() war nötig), und Array-Mutationen mussten über gepatchte Methoden wie push(), splice() und sort() erfolgen. Mit Proxy in Vue 3 fallen diese Beschränkungen weg — neue Properties sind automatisch reaktiv, Array-Indizes und length werden korrekt getrackt. Das Vue 3 Reaktivitätssystem ist damit vollständiger und konsistenter als sein Vorgänger.

Proxy funktioniert nur mit Objekten, nicht mit primitiven Werten wie Zahlen, Strings oder Booleans. Das ist der Grund, warum Vue ref() als separates Konstrukt einführt: Für primitive Werte wird ein Wrapper-Objekt { value: ... } erstellt, das dann über Proxy reaktiv gemacht wird. Der Zugriff über .value ist kein stilistischer Entscheid, sondern technische Notwendigkeit: JavaScript kann Lese- und Schreibzugriffe auf primitive Werte nicht über Proxy abfangen. ref.value ist der Proxy-abgefangene Property-Zugriff auf das Wrapper-Objekt.

2. Track und Trigger: wie Dependency-Tracking funktioniert

Das Vue Reaktivitätssystem verwaltet aktive Effekte über einen globalen Stack. Wenn eine reaktive Funktion ausgeführt wird — ein Template-Render, ein computed()-Getter oder ein watchEffect()-Callback — wird sie auf diesen Stack gesetzt und gilt als "aktiver Effekt". Jeder get-Zugriff auf ein reaktives Property ruft intern track() auf: Vue speichert die Verbindung zwischen dem reaktiven Property und dem aktuellen aktiven Effekt. Diese Verbindungsstruktur ist eine Map von Targets zu Maps von Properties zu Sets von Effekten — das Dependency-Graph des Vue Reaktivitätssystems.

Wenn ein reaktives Property gesetzt wird, ruft der Proxy-Handler trigger() auf: Vue sucht alle Effekte, die dieses Property als Dependency haben, und führt sie erneut aus. Template-Render-Effekte werden dabei nicht sofort synchron ausgeführt, sondern in einer asynchronen Queue für das nächste Microtask gesammelt. Das ist der Grund, warum mehrere State-Änderungen in einem Synchroncode-Block nur ein einziges Re-Render auslösen — Vue batcht Updates. Mit nextTick() kann man auf den Abschluss dieses Microtask warten und dann auf den aktualisierten DOM zugreifen.

3. ref(): Primitives reaktiv machen

ref() ist das Hauptwerkzeug des Vue Reaktivitätssystems für einzelne Werte. Der Aufruf ref(0) erzeugt ein Objekt { value: 0 }, dessen value-Property über Proxy reaktiv gemacht wird. In <script setup> wird eine Ref über .value gelesen und gesetzt. In Templates entfällt das .value — Vue unwrapped Refs im Template-Kontext automatisch. Das ist eine Design-Entscheidung: Im JavaScript-Kontext ist die explizite Ref-API konsistenter; im Template wäre .value überall redundante Syntax.

ref() funktioniert mit jedem Typ: primitiven Werten, Objekten, Arrays, sogar anderen Refs. Wenn man ein Objekt in eine Ref packt — ref({ name: 'Vue', version: 3 }) — wird das Objekt selbst über reactive() reaktiv gemacht. Das heißt: myRef.value.name = 'Nuxt' ist reaktiv, ohne myRef.value ersetzen zu müssen. Das unterscheidet sich von einer einfachen Ref auf einen primitiven Wert: Hier ist nur die Zuweisung von myRef.value = neuerWert reaktiv getrackt. Beim Lesen und Schreiben von Object-Properties direkt auf myRef.value übernimmt der innere Proxy.


// ref() — the primary reactive primitive in Vue 3
import { ref, isRef, unref } from 'vue'

// Primitive value — .value access is the tracked property
const count = ref(0)
count.value++  // triggers re-render of components that read count.value

// Object ref — inner object is made reactive via reactive()
const user = ref({ name: 'Alice', role: 'admin' })
user.value.name = 'Bob'  // reactive — inner object is a Proxy
user.value = { name: 'Charlie', role: 'user' }  // also reactive — replaces entire ref

// Template ref — DOM element reference, starts as null
const inputEl = ref<HTMLInputElement | null>(null)
// <input ref="inputEl" /> — Vue sets inputEl.value to the DOM element after mount

// isRef and unref utilities
console.log(isRef(count))    // true
console.log(unref(count))    // 0 — same as count.value for refs, or the value itself for non-refs

// Ref in reactive() — automatically unwrapped (no .value needed)
import { reactive } from 'vue'
const state = reactive({ count, message: 'hello' })
state.count  // 0 — no .value needed inside reactive()
state.count++  // updates count.value, reactive

4. reactive(): Objekte reaktiv machen

reactive() erzeugt direkt einen Proxy für ein Objekt. Kein Wrapper, kein .value — der Property-Zugriff erfolgt direkt: state.count statt state.count.value. Das klingt bequemer als ref(), hat aber eine entscheidende Einschränkung: Vue Reaktivität geht verloren, wenn das Objekt destrukturiert wird. const { count } = reactive({ count: 0 })count ist jetzt eine normale Zahl, keine Proxy-abgefangene Property. Änderungen an count sind nicht reaktiv. Das ist der häufigste Fehler beim Einsatz von reactive().

Die Lösung für sicheres Destrukturieren aus reactive()-Objekten ist toRefs(): Es konvertiert alle Properties eines reaktiven Objekts in einzelne Refs. Dadurch behält jede destrukturierte Property ihre Reaktivität über das .value-Binding. reactive() eignet sich besonders für zusammengehörige State-Gruppen, die als Einheit behandelt werden — z.B. ein Formularobjekt mit vielen Feldern. In Pinia-Stores nutzt der Setup-Style ref() für einzelne Werte statt reactive() für den gesamten State, weil dies die Typsicherheit und das DevTools-Tracking verbessert.

5. ref vs. reactive: wann welches einsetzen

Die Frage "ref oder reactive?" beschäftigt jeden Entwickler beim Einstieg in das Vue 3 Reaktivitätssystem. Die Antwort der Vue-Core-Team lautet inzwischen klar: Bevorzugt ref(). Der Grund: ref() ist konsistenter. Es funktioniert für alle Typen, sein .value-Interface ist eindeutig und macht reaktive Werte in Code sofort erkennbar. Wenn man sieht, dass etwas mit .value zugegriffen wird, weiß man sofort: Das ist ein reaktiver Wert. Bei reactive()-Objekten ist ohne Typdefinition nicht sichtbar, ob ein Property reaktiv ist oder nicht.

reactive() macht Sinn, wenn man eine Gruppe eng zusammengehöriger Werte als natürliche Einheit modellieren will und niemals destrukturieren muss. Ein Formularobjekt const form = reactive({ email: '', password: '', remember: false }) und immer als form.email, form.password angesprochen — das ist ein valider reactive()-Einsatzfall. Für alles andere: ref(). In Pinia Stores, Composables und dem allgemeinen Composition-API-Code hat ref() die Oberhand, weil es sich in TypeScript besser verhält und keinen Destrukturierungsverlust kennt.

6. computed(): reaktive Berechnungen mit Caching

computed() im Vue Reaktivitätssystem ist eine Ref mit automatischem Caching. Beim ersten Zugriff auf computed.value führt Vue den Getter-Callback aus und speichert das Ergebnis. Bei jedem weiteren Zugriff gibt Vue das gecachte Ergebnis zurück — ohne den Getter erneut auszuführen — solange sich keine der im Getter gelesenen reaktiven Werte verändert haben. Das ist das Kernversprechen von computed(): gleiche Inputs → gecachtes Ergebnis, andere Inputs → Neuberechnung.

Der häufigste Missbrauch von computed() ist der Getter mit Seiteneffekten. Ein Getter, der einen API-Aufruf macht, einen Store-State schreibt oder einen globalen Wert setzt, ist falsch eingesetzt. computed()-Getter müssen rein funktional sein: Gleiches Input → gleicher Output, keine Seiteneffekte. Für Seiteneffekte bei Reaktivitätsänderungen ist watch() das richtige Werkzeug. Der zweite häufige Fehler: Computed-Properties für parametrisierte Abfragen — const getItem = computed(() => (id: string) => items.value.find(i => i.id === id)). Der äußere computed() wird nie ungültig, weil er keine reaktiven Abhängigkeiten im äußeren Scope liest. Für parametrisierte Abfragen ist eine reguläre Funktion oder ein Map-basierter Cache im Store die bessere Lösung.


// computed() — cached reactive derivation
import { ref, computed } from 'vue'

const items = ref([
  { id: '1', name: 'Vue 3', price: 0, category: 'framework' },
  { id: '2', name: 'Pinia', price: 0, category: 'state' },
  { id: '3', name: 'Vite', price: 0, category: 'tooling' },
])
const filterCategory = ref('all')
const searchQuery = ref('')

// Cached: only recalculates when items.value or filterCategory.value changes
const filteredItems = computed(() => {
  const category = filterCategory.value
  const query = searchQuery.value.toLowerCase()

  return items.value.filter(item => {
    const matchesCategory = category === 'all' || item.category === category
    const matchesSearch = item.name.toLowerCase().includes(query)
    return matchesCategory && matchesSearch
  })
})

// Writable computed — getter + setter
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value: string) => {
    const [first, ...rest] = value.split(' ')
    firstName.value = first
    lastName.value = rest.join(' ')
  },
})
// fullName.value = 'Alice Wonderland' — updates both firstName and lastName

7. watch() und watchEffect(): Seiteneffekte reaktiv steuern

watch() und watchEffect() sind die Werkzeuge für Seiteneffekte im Vue Reaktivitätssystem. Der Unterschied ist fundamental: watchEffect() führt den Callback sofort aus, trackt automatisch alle reaktiven Werte, die im Callback gelesen werden, und führt ihn erneut aus, wenn sich einer dieser Werte ändert. watch() ist explizit: Man gibt die beobachteten Quellen an, und der Callback erhält alten und neuen Wert als Parameter. watch() führt den Callback nicht sofort aus (außer mit { immediate: true }) und eignet sich daher für Reaktionen auf Wertänderungen, die den Vorwert kennen müssen.

watchEffect() ist einfacher, hat aber eine Falle: Weil es alle reaktiven Zugriffe im Callback automatisch trackt, kann es mehr Abhängigkeiten aufbauen als beabsichtigt. Eine Hilfsfunktion, die intern ein weiteres reaktives Objekt liest, wird automatisch als Dependency registriert — auch wenn diese Abhängigkeit konzeptionell nicht gewollt ist. In diesen Fällen ist watch() mit expliziten Quellen die kontrolliertere Wahl. Beide Varianten geben eine Stopp-Funktion zurück und beide akzeptieren onCleanup-Callbacks für aufzuräumende Seiteneffekte — das Muster für Event-Listener und Timer in Composables.

8. Reaktivitätsverlust: die häufigsten Fehler

Reaktivitätsverlust ist der häufigste Bug beim Einstieg in das Vue Reaktivitätssystem. Die häufigste Ursache: Destrukturierung. Jeder const { count } = reactiveObjOrStore-Zugriff ohne toRefs() oder storeToRefs() erzeugt eine nicht-reaktive Kopie. Das gilt für reactive()-Objekte, für Pinia-Stores und für Composable-Rückgaben, die intern reactive() verwenden. Eine einfache Regel: Wenn aus einem reaktiven Objekt destrukturiert wird, immer toRefs() verwenden. Wenn aus einem Pinia-Store destrukturiert wird, immer storeToRefs() für State und Getters verwenden.

Der zweite häufige Reaktivitätsverlust: Reaktive Werte außerhalb des reaktiven Kontexts referenzieren. Wenn eine reaktive Variable einer normalen Variable zugewiesen wird — const value = myRef.value statt const value = myRef — verliert value die Reaktivitätsbindung. Änderungen an myRef.value aktualisieren value nicht. Das ist kein Bug, sondern JavaScript-Semantik: Primitive Werte werden by value kopiert. Nur Objekte werden by reference übergeben. ref()-Wrapper lösen das, weil das Wrapper-Objekt by reference übergeben wird — nur .value ist der primitive Inhalt.


// Common Vue 3 reactivity loss patterns and fixes

import { ref, reactive, toRefs, computed, watchEffect } from 'vue'

// WRONG: destructuring from reactive() loses reactivity
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state  // both are now plain values, not reactive

// RIGHT: toRefs() preserves reactivity via ref wrapping
const { count, name } = toRefs(state)
// count.value and name.value are now reactive refs synced to state

// WRONG: assigning .value to a variable loses the reactive binding
const total = computed(() => count.value * 2)
const snapshot = total.value  // plain number — no longer reactive

// RIGHT: pass the ref/computed itself, read .value only when needed
function processTotal(totalRef: Ref<number>) {
  watchEffect(() => {
    console.log('Total updated:', totalRef.value)  // reactive read
  })
}
processTotal(total)  // pass the computed ref, not its value

// WRONG: replacing reactive() with a new object breaks the proxy
const form = reactive({ email: '', password: '' })
// form = { email: 'a@b.com', password: 'x' }  // ERROR: cannot reassign const
// OR if using let: loses all reactive subscriptions on the old proxy object

// RIGHT: update properties in place
form.email = 'a@b.com'
form.password = 'x'
// OR use Object.assign() to update all properties at once
Object.assign(form, { email: 'a@b.com', password: 'x' })

9. Vergleich der reaktiven APIs

Das Vue 3 Reaktivitätssystem bietet mehrere APIs für reaktive Daten. Die Wahl hängt vom Anwendungsfall ab. Diese Tabelle zeigt die wichtigsten Unterschiede.

API Für was Achtung Wann bevorzugt
ref() Jeden Typ — primitiv oder Objekt .value in JS-Context nicht vergessen Standard — immer bevorzugen
reactive() Objekte mit vielen Properties Destrukturierung bricht Reaktivität Formularobjekte, nie destrukturiert
computed() Abgeleitete, gecachte Werte Kein Seiteneffekt im Getter Alle abgeleiteten Daten
watch() Explizite Quellen beobachten Läuft nicht sofort (ohne immediate) Wenn alt/neu-Wert gebraucht wird
watchEffect() Alle gelesenen Werte automatisch Kann ungewollte Deps aufbauen Initialer Aufruf gewünscht, deps unklar

Neben diesen Basis-APIs gibt es hilfreiche Utilities im Vue Reaktivitätssystem: toRef() erstellt eine Ref für ein einzelnes Property eines reaktiven Objekts (statt toRefs() für alle). shallowRef() und shallowReactive() machen nur die oberste Ebene reaktiv — für große Objekte, bei denen tief verschachtelte Reaktivität unnötig ist. readonly() erstellt einen Proxy, der Schreibzugriffe verhindert und in der Konsole warnt — nützlich für State, der nur gelesen werden soll. markRaw() markiert ein Objekt als nicht-reaktiv, damit Vue keinen Proxy dafür erstellt — sinnvoll für externe Library-Objekte wie Three.js-Szenen oder Chart-Instanzen.

Mironsoft

Vue 3 Architektur und Composition API Expertise

Vue 3 Reaktivitätsfehler in eurem Projekt beheben?

Wir analysieren bestehenden Vue 3-Code auf Reaktivitätsverlust, falsch eingesetzte computed()-Getter und ineffizientes Watch-Handling — und bringen eure Composition API auf modernen Standard.

Reaktivitäts-Audit

Systematische Analyse auf Reaktivitätsverlust durch Destrukturierung und falsche reactive()-Nutzung

Composable-Refactoring

Composables auf ref()-Standard umstellen und watchEffect-Cleanup sicherstellen

TypeScript-Integration

Ref-Typen, computed-Return-Typen und watch-Source-Typen vollständig korrekt deklarieren

10. Zusammenfassung

Das Vue 3 Reaktivitätssystem ist eine elegante Implementierung von Dependency-Tracking über JavaScript Proxy. ref() ist die Standard-API für alle reaktiven Werte — sie ist konsistent, typsicher und macht reaktive Werte durch .value sofort erkennbar. reactive() ist die Alternative für Objekte, die als Einheit behandelt werden, ohne Destrukturierung. computed() cacht reaktive Berechnungen und sollte für alle abgeleiteten Daten verwendet werden — mit rein funktionalen Gettern ohne Seiteneffekte. watch() und watchEffect() sind die Werkzeuge für Seiteneffekte, die auf Reaktivitätsänderungen reagieren müssen.

Die häufigsten Fehler im Vue 3 Reaktivitätssystem sind Reaktivitätsverlust durch Destrukturierung ohne toRefs() oder storeToRefs(), und das Zuweisen von .value zu einer normalen Variable statt die Ref selbst weiterzugeben. Wer das Proxy-basierte Dependency-Tracking versteht — was track() aufruft, was trigger() aufruft und wann Vue Updates batcht — hat das mentale Modell, um jeden Reaktivitätsfehler systematisch zu debuggen statt zu raten.

Vue 3 Reaktivität — Das Wichtigste auf einen Blick

ref() vs reactive()

ref() als Standard für alle Typen. reactive() nur für Objekte, die nie destrukturiert werden. Destrukturierung aus reactive() immer mit toRefs().

computed() richtig einsetzen

Nur für rein funktionale Berechnungen — kein Seiteneffekt im Getter. Cacht Ergebnis bis Abhängigkeiten sich ändern. Für Seiteneffekte: watch().

watch vs. watchEffect

watchEffect: sofortiger Aufruf, automatisches Dependency-Tracking. watch: explizite Quellen, alt/neu-Werte, kein sofortiger Aufruf (ohne immediate).

Reaktivitätsverlust

Destrukturierung ohne toRefs()/storeToRefs() verliert Reaktivität. .value zuweisen statt Ref weitergeben verliert Reaktivität. Ref immer als Objekt weitergeben.

11. FAQ: Vue 3 Reaktivität

1Warum .value in JS, nicht im Template?
Primitive können nicht per Proxy abgefangen werden — ref() wrappet sie in { value: ... }. Im Template unwrapped Vue Refs automatisch. In JS macht .value reaktive Werte erkennbar.
2Vue 2 vs. Vue 3 Reaktivität?
Vue 2: Object.defineProperty(), Vue.set() nötig für neue Properties, Array-Patches. Vue 3: Proxy, neue Properties automatisch reaktiv, Array-Index-Tracking korrekt.
3reactive() verliert Reaktivität bei Destrukturierung?
Proxy fängt nur Property-Zugriffe auf dem Objekt ab. Destrukturierung kopiert den Wert, Proxy ist nicht mehr beteiligt. Lösung: toRefs().
4watch() vs. watchEffect()?
watch(): explizite Quellen, alt/neu-Werte, kein sofortiger Aufruf. watchEffect(): automatisches Tracking, sofortiger Aufruf, keine alt/neu-Werte.
5computed() vs. watchEffect()?
computed(): abgeleiteter Wert, gecacht, kein Seiteneffekt. watchEffect(): Seiteneffekte ausführen, kein Rückgabewert, kein Caching.
6reactive() mit neuem Objekt ersetzen?
Nein — Komponenten verlieren Verbindung zum alten Proxy. Properties in-place aktualisieren: Object.assign(state, neueWerte) oder einzelne Zuweisung.
7Wofür markRaw()?
Verhindert Proxy-Erstellung für externe Library-Objekte (Three.js, Chart.js). Spart Performance und verhindert Fehler bei Objekten, die nicht proxyfähig sind.
8Was ist shallowRef()?
Nur .value-Zuweisung ist reaktiv, nicht Mutations des Objektinhalts. Für große Objekte, die als Ganzes ausgetauscht werden — nie partiell mutiert.
9Warum batcht Vue Updates?
Mehrere State-Änderungen im selben Synchroncode-Block lösen nur ein Re-Render aus. Mit nextTick() oder await nextTick() auf aktualisierten DOM warten.
10computed() updatet nicht wie erwartet?
computed() invalidiert nur bei Änderung reaktiver Abhängigkeiten. Häufige Ursache: Closure-Variable statt reaktiver Wert im Getter. Alle Inputs müssen ref()/reactive() sein.