sauber halten – von Prop-Mutation bis v-model
Props fließen nach unten, Emits fließen nach oben – der One-Way Data Flow in Vue.js ist kein Design-Dogma, sondern die Grundlage für vorhersehbares, debugbares Komponentenverhalten. Wer Props mutiert, baut Bugs, die erst unter Last oder bei komplexen Zustandsänderungen sichtbar werden.
Inhaltsverzeichnis
- 1. One-Way Data Flow: Das Prinzip hinter Props und Emits
- 2. defineProps mit TypeScript: typsichere Props
- 3. Prop-Validierung: Typen, Required und Validator
- 4. Prop-Mutation: Warum sie gefährlich ist
- 5. defineEmits: Typsichere Ereignisse nach oben
- 6. v-model mit Komponenten: das update:modelValue-Pattern
- 7. Mehrfaches v-model und benannte Props
- 8. Objekte und Arrays als Props: Fallen und Muster
- 9. Props-Muster im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. One-Way Data Flow: Das Prinzip hinter Props und Emits
Der One-Way Data Flow in Vue.js beschreibt das grundlegende Kommunikationsmuster zwischen Eltern- und Kindkomponenten: Daten fließen über Props von der Elternkomponente zur Kindkomponente, und Ereignisse fließen über Emits von der Kindkomponente zurück zur Elternkomponente. Dieses unidirektionale Muster macht den Zustand einer Anwendung vorhersehbar und nachvollziehbar – weil jede Zustandsänderung immer von einem eindeutigen Auslöser an einer eindeutigen Stelle ausgeht. Das ist kein Zufall, sondern ein bewusstes Design-Entscheidung, die Vue.js von älteren Bibliotheken unterscheidet, die bidirektionale Datenbindung überall erlaubten.
Ohne One-Way Data Flow entstehen die klassischen Probleme reaktiver Systeme: Eine Kindkomponente ändert Daten, die gleichzeitig von der Elternkomponente und anderen Geschwistern gelesen werden. Es ist nicht mehr klar, welche Komponente für welche Änderung verantwortlich ist. Debugging wird zur Detektivarbeit, weil Zustandsänderungen aus mehreren Richtungen kommen können. Das One-Way Data Flow-Prinzip verhindert genau das: Jede Zustandsänderung geht durch die Elternkomponente, die als einzige Instanz entscheidet, ob und wie sich der Zustand ändert. Die Kindkomponente schlägt Änderungen nur vor, sie erzwingt sie nicht.
2. defineProps mit TypeScript: typsichere Props
In Vue 3 mit der Composition API und <script setup> ist defineProps der Standardweg, Props zu deklarieren. Mit TypeScript bietet defineProps vollständige Typ-Inferenz: Die Props sind im Template und im Script-Block typisiert, ohne Laufzeitfehler abwarten zu müssen. Die generische Syntax defineProps<{ title: string; count: number; items: string[] }>() ist kompakter als die Laufzeit-Objekt-Syntax und profitiert direkt vom TypeScript-Compiler für Fehlermeldungen.
Standardwerte für Props mit der TypeScript-Syntax setzt man über withDefaults: const props = withDefaults(defineProps<Props>(), { count: 0, items: () => [] }). Wichtig: Für Props die Objekte oder Arrays als Defaultwert haben, muss die Factory-Funktion-Syntax () => [] statt des direkten Werts verwendet werden – sonst teilen sich alle Instanzen der Komponente dasselbe Array-Objekt, was zu unerwarteten Nebeneffekten führt. Dieser Fehler ist subtil und schwer zu debuggen, weil er nur bei mehreren gleichzeitigen Komponenteninstanzen auftritt.
// components/ProductCard.vue — TypeScript props with defaults
interface Props {
title: string
price: number
imageUrl?: string
tags?: string[]
isAvailable?: boolean
variant?: 'default' | 'compact' | 'featured'
}
const props = withDefaults(defineProps<Props>(), {
imageUrl: '/images/placeholder.jpg',
tags: () => [], // factory function — each instance gets its own array
isAvailable: true,
variant: 'default',
})
// Props are reactive — use in template directly or via toRefs for destructuring
const { title, price } = toRefs(props) // reactive references to individual props
3. Prop-Validierung: Typen, Required und Validator
Neben der TypeScript-basierten Typisierung bietet Vue auch Laufzeit-Prop-Validierung, die unabhängig von TypeScript funktioniert und in der Entwicklungsumgebung Warnungen ausgeben. Das ist besonders wichtig für Bibliotheks-Komponenten, die von anderen Teams oder in JavaScript-Projekten (ohne TypeScript) verwendet werden. Die Laufzeit-Validierung erlaubt auch benutzerdefinierte Validator-Funktionen: validator: (value) => ['primary', 'secondary', 'danger'].includes(value). Diese Validatoren werden in der Entwicklungsumgebung bei jedem Prop-Update ausgeführt und geben bei Verstößen eine Konsolenwarnung aus.
Ein häufiger Fehler beim Deklarieren von Props: Komplexe Objekt-Types werden als Object deklariert statt als konkretes Interface. Das verhindert TypeScript-Autovervollständigung für die Properties des Objekts und macht Refactoring gefährlich – Properties können umbenannt werden ohne dass der Compiler warnt. Mit der generischen defineProps<T>()-Syntax und präzisen Interface-Definitionen wird jede falsche Prop-Nutzung vom Compiler abgefangen, bevor der Code in den Browser kommt. Das reduziert Laufzeitfehler drastisch und beschleunigt die Entwicklung durch präzise Autovervollständigung.
4. Prop-Mutation: Warum sie gefährlich ist
Prop-Mutation ist einer der häufigsten Fehler bei Vue-Entwicklern, die von anderen Frameworks kommen oder die das One-Way Data Flow-Prinzip nicht vollständig verinnerlicht haben. In der Praxis sieht das so aus: Eine Kindkomponente erhält ein Objekt als Prop und modifiziert direkt eine Property dieses Objekts – props.user.name = 'Neuer Name'. In Vue 3 ist das technisch möglich, weil Objekt-Props nicht tief eingefroren werden. Vue gibt zwar eine Konsolenwarnung aus, wenn eine direkte Prop-Referenz überschrieben wird (props.user = newUser), aber die direkte Mutation von Objekt-Properties wird nicht verhindert.
Das Problem mit Prop-Mutation: Sie verletzt den One-Way Data Flow und macht Zustandsänderungen unsichtbar. Wenn Komponente A, B und C alle auf dasselbe Objekt als Prop zeigen und Komponente B eine Property ändert, sehen alle drei Komponenten die Änderung – aber niemand hat ein Emit emittiert, niemand hat einen Store aktualisiert, und in den Vue DevTools ist keine Zustandsänderung nachvollziehbar. Das macht Debugging extrem schwer. Die korrekte Alternative: Die Kindkomponente emittiert das gewünschte Update via emit('update:user', { ...props.user, name: 'Neuer Name' }), und die Elternkomponente entscheidet, ob und wie der Zustand aktualisiert wird.
// WRONG: Direct prop mutation — violates One-Way Data Flow
// components/UserForm.vue
const props = defineProps<{ user: User }>()
// This mutates the shared object — parent and siblings see the change
// Vue warns on direct reassignment but not on deep property mutation
function handleNameChange(name: string) {
props.user.name = name // NEVER do this — breaks One-Way Data Flow
}
// RIGHT: Emit the desired change — parent decides what to update
const emit = defineEmits<{ 'update:user': [user: User] }>()
function handleNameChange(name: string) {
// Create a new object — immutability at prop boundary
emit('update:user', { ...props.user, name })
}
// RIGHT alternative: use local copy for form state
const localUser = ref({ ...props.user }) // initialize from prop once
watch(() => props.user, (newUser) => { localUser.value = { ...newUser } })
5. defineEmits: Typsichere Ereignisse nach oben
defineEmits ist das Gegenstück zu defineProps und deklariert die Ereignisse, die eine Komponente nach oben emittiert. Mit der TypeScript-Syntax defineEmits<{ 'event-name': [payload: Type] }>() sind die Event-Payloads vollständig typisiert – sowohl für die emittierende Komponente als auch für die übergeordnete Komponente, die das Event in einem Template-Event-Handler empfängt. Der TypeScript-Compiler prüft, dass der übergebene Payload dem deklarierten Typ entspricht, und gibt bei Abweichungen sofort einen Fehler aus.
Eine wichtige Funktion von defineEmits: Es aktiviert die Laufzeit-Validierung der Emits in der Entwicklungsumgebung. Wird ein Event emittiert das nicht in defineEmits deklariert ist, gibt Vue eine Konsolenwarnung aus. Das verhindert Typos in Event-Namen – ein häufiger Bug, bei dem eine Komponente emit('udpate:value') (Typo) statt emit('update:value') ruft und die Elternkomponente das Event nie empfängt. Mit defineEmits wird dieser Fehler sofort sichtbar, nicht erst zur Laufzeit wenn sich ein Nutzer über eine nicht funktionierende Eingabe beschwert.
6. v-model mit Komponenten: das update:modelValue-Pattern
Das v-model-Direktiv auf einer Komponente ist syntaktischer Zucker für das Props-und-Emits-Pattern: <MyInput v-model="username" /> entspricht <MyInput :modelValue="username" @update:modelValue="username = $event" />. Die Komponente empfängt den Wert über den Prop modelValue und sendet Änderungen über das Emit update:modelValue zurück. Dieses Muster implementiert den One-Way Data Flow für Two-Way-Binding: Der Wert fließt als Prop hinein, und Änderungsvorschläge fließen als Emits heraus – die Elternkomponente aktualisiert dann ihren eigenen Zustand.
Ein häufiger Fehler bei der v-model-Implementierung: Die Kindkomponente verwendet v-model auf einem Input und bindet es direkt an modelValue. Das führt zu einer direkten Prop-Mutation, wenn der Nutzer tippt. Die korrekte Implementierung: :value="modelValue" (nicht v-model) auf dem Input, und ein @input-Handler der emit('update:modelValue', event.target.value) aufruft. Alternativ kann man v-model auf dem Input verwenden, wenn man einen berechneten Setter nutzt: computed({ get: () => props.modelValue, set: (val) => emit('update:modelValue', val) }).
// components/AppInput.vue — Correct v-model implementation
const props = defineProps<{
modelValue: string
label?: string
error?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
blur: []
}>()
// Computed with getter/setter — bridges modelValue prop to v-model on native input
const value = computed({
get: () => props.modelValue,
set: (val: string) => emit('update:modelValue', val),
})
// Usage in template: v-model="value" on the native <input>
// No direct prop mutation — One-Way Data Flow maintained
// Usage in parent:
// <AppInput v-model="formData.email" label="E-Mail" :error="errors.email" />
7. Mehrfaches v-model und benannte Props
Seit Vue 3 unterstützen Komponenten mehrere v-model-Bindings gleichzeitig, mit benutzerdefinierten Namen statt dem Standard modelValue. Die Syntax <DateRangePicker v-model:start="startDate" v-model:end="endDate" /> bindet zwei Props gleichzeitig über das One-Way Data Flow-Pattern. Die Komponente empfängt start und end als Props und emittiert update:start und update:end als Ereignisse. Das ist deutlich sauberer als ein einzelnes v-model mit einem Objekt als Wert, weil jeder Wert einzeln aktualisiert werden kann ohne das gesamte Objekt zu ersetzen.
Benannte v-model-Bindings eignen sich besonders für komplexe Formular-Komponenten wie Datums-Picker, Adress-Formulare oder Konfigurations-Panels, die mehrere unabhängige Werte verwalten. In der Praxis sieht man häufig Komponenten, die ein einzelnes großes Objekt über v-model binden und dann tief in dieses Objekt schreiben – das verletzt den One-Way Data Flow, weil die Kindkomponente das Objekt direkt mutiert statt ein neues Objekt zu emittieren. Mit mehreren benannten v-model-Bindings wird jede Property einzeln behandelt und die Immutabilität an der Prop-Grenze beibehalten.
8. Objekte und Arrays als Props: Fallen und Muster
Objekte und Arrays als Props sind eine häufige Quelle von subtilen Bugs im One-Way Data Flow. In JavaScript werden Objekte und Arrays als Referenz übergeben – wenn die Elternkomponente ein Objekt als Prop übergibt und die Kindkomponente eine Property dieses Objekts ändert, ändert sich das Original in der Elternkomponente mit. Vue erkennt diese indirekte Mutation nicht als Reaktivitätsverletzung und gibt keine Warnung aus. Das führt zu Bugs, die schwer zu reproduzieren sind, weil sie nur bei bestimmten Sequenzen von Zustandsänderungen auftreten.
Das korrekte Muster für komplexe Props: An der Grenze zwischen Eltern- und Kindkomponente immer mit flachen Kopien arbeiten. Wenn die Kindkomponente ein Formular darstellt, das Felder eines übergebenen Objekts bearbeitet, initialisiert sie einen lokalen Zustand mit einer flachen Kopie: const localData = reactive({ ...props.item }). Beim Speichern emittiert sie das geänderte Objekt: emit('save', { ...localData }). Die Elternkomponente entscheidet dann, ob und wie der übergeordnete Zustand aktualisiert wird – gemäß dem One-Way Data Flow-Prinzip.
9. Props-Muster im Vergleich
Die Wahl des richtigen Props-und-Emits-Musters hängt vom Anwendungsfall ab. Die folgende Tabelle vergleicht die häufigsten Szenarien und empfehlenswerte Lösungen.
| Szenario | Falsches Muster | Richtiges Muster | Warum |
|---|---|---|---|
| Input-Komponente | v-model direkt auf Prop | computed getter/setter | Kein direktes Props-Mutieren |
| Formular mit Objekt | props.item.name = val | Lokale Kopie + emit('save') | Elternkomponente bleibt Eigentümer |
| Mehrere Werte binden | Einzelnes v-model Objekt | Mehrere benannte v-model | Jeder Wert einzeln updatebar |
| Array-Prop modifizieren | props.items.push(item) | emit('add', item) | Mutation über Komponentengrenze vermeiden |
| Booleanscher Toggle | props.isOpen = !props.isOpen | emit('update:isOpen', !props.isOpen) | One-Way Data Flow gewahrt |
Die Tabelle macht deutlich: Das falsche Muster ist in den meisten Fällen eine direkte Mutation des Props. Das richtige Muster emittiert immer eine neue Version der Daten und überlässt der Elternkomponente die Entscheidung über die Zustandsänderung. Dieser scheinbar aufwändigere Weg zahlt sich aus, sobald eine Komponente in mehreren Kontexten mit unterschiedlichen Anforderungen verwendet wird – die Elternkomponente kann das Emit ignorieren, transformieren oder ablehnen, je nach Kontext.
Mironsoft
Vue.js · Vue 3 · Komponenten-Architektur · TypeScript
Saubere Vue-Komponentenarchitektur für euer Projekt?
Wir reviewen Vue-Codebasen auf Props-Mutations, One-Way-Data-Flow-Verletzungen und fehlende TypeScript-Typen – und refaktorieren zu wartbarer, debugbarer Komponentenarchitektur.
Code-Review
Props-Mutations, fehlende Emits-Deklarationen und One-Way-Flow-Verletzungen identifizieren
TypeScript-Migration
defineProps und defineEmits mit vollständigen TypeScript-Typen nachrüsten
Refactoring
Prop-Mutationen durch korrekte Emits und lokale Zustandskopien ersetzen
10. Zusammenfassung
Der One-Way Data Flow mit Props und Emits in Vue.js ist kein akademisches Konzept, sondern die praktische Grundlage für vorhersehbares Komponentenverhalten. Props fließen nach unten und dürfen von der Kindkomponente niemals direkt mutiert werden – weder bei primitiven Typen noch bei Objekten oder Arrays. Stattdessen emittiert die Kindkomponente das gewünschte Update und die Elternkomponente entscheidet über die Zustandsänderung. Das macht jeden Zustandsübergang in den Vue DevTools sichtbar und debuggbar.
Mit defineProps<T>(), withDefaults und defineEmits<T>() in TypeScript sind Props und Emits vollständig typisiert, was Typos in Event-Namen und falsche Prop-Typen vom Compiler abfangen lässt. Das v-model-Pattern mit computed getter/setter implementiert Two-Way-Binding korrekt ohne den One-Way Data Flow zu verletzen. Mehrere benannte v-model-Bindings ersetzen komplexe Objekt-Props durch individuelle, einzeln updatebare Werte. Das Ergebnis: Komponenten die unabhängig testbar, unabhängig wiederverwendbar und zuverlässig debuggbar sind.
Vue Props, Emits und One-Way Data Flow — Das Wichtigste auf einen Blick
One-Way Data Flow
Props fließen nach unten, Emits nach oben. Keine Prop-Mutation. Jede Zustandsänderung geht durch die Elternkomponente.
TypeScript-Props
defineProps mit generischer Typsyntax. withDefaults für Standardwerte. Array/Objekt-Defaults als Factory-Funktion.
v-model korrekt
computed getter/setter: get gibt modelValue zurück, set emittiert update:modelValue. Kein direktes v-model auf Props.
Objekte als Props
Lokale Kopie bei Formularen: reactive({ ...props.item }). Bei Änderungen emit('save', { ...localData }) – nie direkt mutieren.