<v/>
{ }
Vue.js · Vue 3 · Props · Emits · One-Way Data Flow
Vue Props, Emits und One-Way Data Flow
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.

14 Min. Lesezeit defineProps · defineEmits · v-model · withDefaults · TypeScript Vue 3 · Composition API · Single File Components

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.

11. FAQ: Vue Props, Emits und One-Way Data Flow

1Was ist One-Way Data Flow?
Props fließen nach unten, Emits nach oben. Kindkomponente schlägt Änderungen vor, Elternkomponente entscheidet. Macht Zustand vorhersehbar und debuggbar.
2Warum keine Props mutieren?
Mutationen erscheinen nicht in Vue DevTools, sind unsichtbar und führen bei geteilten Zuständen zu unkontrollierbaren Nebeneffekten.
3Standardwerte mit TypeScript?
withDefaults(defineProps<T>(), { ... }). Arrays/Objekte als Factory-Funktion: items: () => [] – nicht items: [] sonst geteiltes Objekt.
4v-model korrekt implementieren?
computed getter gibt modelValue zurück, setter emittiert update:modelValue. Kein v-model direkt auf Props binden.
5Objekt als Prop im Formular?
Lokale Kopie: reactive({ ...props.item }). Formular arbeitet mit Kopie. Beim Speichern emit('save', { ...localData }). Nie direkt mutieren.
6Wann mehrere benannte v-models?
Bei mehreren unabhängigen Werten: v-model:start, v-model:end. Besser als ein Objekt-v-model – jeder Wert einzeln updatebar.
7Array-Props korrekt behandeln?
Niemals push/splice direkt. emit('add', item) oder emit('update:items', [...props.items, item]) – Elternkomponente erstellt neues Array.
8Emits typisieren mit TypeScript?
defineEmits<{ 'update:value': [value: string]; 'submit': [data: FormData] }>(). TypeScript prüft Payload-Typen beim emit()-Aufruf.
9Unterschied defineProps TypeScript vs. Laufzeit?
Generische Syntax zur Compile-Zeit geprüft, Autovervollständigung. Laufzeit-Syntax gibt Konsolenwarnungen. Für TypeScript-Projekte immer generische Syntax.
10v-model direkt auf Prop binden?
Führt zu direkter Prop-Mutation beim Tippen. Vue gibt Laufzeit-Warnung. Immer computed getter/setter als Vermittlung verwenden.