Vue 3 Script Setup und defineModel 2026: Moderne Syntax und Interview-Fragen

Umfassender Leitfaden zu Vue 3 script setup und defineModel für bidirektionale Datenbindung. Behandelt reaktive Props-Destrukturierung, TypeScript-Integration und typische Interview-Fragen.

Vue 3 Script Setup und defineModel Tutorial

Die Vue 3 Script Setup Syntax revolutioniert die Komponentenentwicklung durch die Eliminierung von Boilerplate-Code und verbesserte TypeScript-Integration. In Kombination mit defineModel, das in Vue 3.3 experimentell eingeführt und in Vue 3.4 stabilisiert wurde, erfordert die Erstellung von Komponenten mit bidirektionaler Datenbindung deutlich weniger Code als das Options API-Äquivalent.

Vue 3.5 Reaktive Props-Destrukturierung

Ab Vue 3.5 können Props direkt aus defineProps destrukturiert werden, während die Reaktivität erhalten bleibt. Dies macht toRefs in den meisten Fällen überflüssig.

Grundlagen von Script Setup in Vue 3.5

Die <script setup> Syntax ist ein Compile-Time-Syntaxzucker für die Composition API innerhalb von Single File Components. Variablen, die auf oberster Ebene deklariert werden, sind automatisch im Template verfügbar, und Importe können direkt ohne Registrierung verwendet werden.

vue
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User } from '@/types'

// Props mit TypeScript-Interface
const props = defineProps<{
  user: User
  showEmail?: boolean
}>()

// Reaktiver State
const isExpanded = ref(false)

// Computed Property
const displayName = computed(() => {
  return `${props.user.firstName} ${props.user.lastName}`
})

// Event Emitter mit typisiertem Payload
const emit = defineEmits<{
  select: [userId: string]
  delete: [userId: string]
}>()

// Methoden sind einfache Funktionen
function handleSelect() {
  emit('select', props.user.id)
}
</script>

<template>
  <div class="user-card" @click="handleSelect">
    <h3>{{ displayName }}</h3>
    <p v-if="showEmail">{{ user.email }}</p>
    <button @click.stop="isExpanded = !isExpanded">
      {{ isExpanded ? 'Einklappen' : 'Aufklappen' }}
    </button>
  </div>
</template>

Der Compiler übernimmt Export-Statements und Komponentenregistrierung automatisch. Dies reduziert die kognitive Belastung im Vergleich zum expliziten Zurückgeben von Werten aus einer setup() Funktion.

Reaktive Props-Destrukturierung (Vue 3.5+)

Vor Vue 3.5 brach die Destrukturierung von Props die Reaktivität, da JavaScript-Destrukturierung einen Snapshot des Wertes zu diesem Zeitpunkt erstellt. Vue 3.5 führt Compiler-Level-Tracking ein, das die Reaktivität für destrukturierte Props erhält.

vue
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watch } from 'vue'

// Destrukturierung mit Defaults - bleibt reaktiv in 3.5+
const { query = '', placeholder = 'Suchen...' } = defineProps<{
  query?: string
  placeholder?: string
}>()

// Watch funktioniert mit destrukturierten Props
watch(
  () => query,
  (newQuery) => {
    console.log('Query geändert:', newQuery)
  }
)
</script>

<template>
  <input
    :value="query"
    :placeholder="placeholder"
    @input="$emit('update:query', ($event.target as HTMLInputElement).value)"
  />
</template>

Diese Syntax erfordert Vue 3.5 oder höher. Die Vue 3.5 Release-Ankündigung dokumentiert das vollständige Verhalten der reaktiven Props-Destrukturierung.

defineModel für Bidirektionale Bindung

Vor defineModel erforderte die Implementierung von v-model für eine benutzerdefinierte Komponente das Deklarieren eines Props und das manuelle Emittieren eines Update-Events. Dieses Muster findet sich in zahlreichen Vue-Codebasen:

vue
<!-- Der alte Weg - ausführlich aber explizit -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

function updateValue(event: Event) {
  emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>

Mit defineModel wird dieselbe Komponente erheblich kürzer:

vue
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel erstellt ein Ref, das mit v-model synchronisiert
const model = defineModel<string>({ default: '' })
</script>

<template>
  <input
    v-model="model"
    type="text"
    class="text-input"
  />
</template>

Die Elternkomponente verwendet die Standard v-model Syntax:

vue
<!-- ParentForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import TextInput from './TextInput.vue'

const username = ref('')
</script>

<template>
  <TextInput v-model="username" />
  <p>Benutzername: {{ username }}</p>
</template>

defineModel gibt ein Ref zurück, das mit dem gebundenen Wert der Elternkomponente synchronisiert bleibt. Mutationen an diesem Ref lösen automatisch das Update-Event aus.

Bereit für deine Vue.js / Nuxt.js-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Benannte Models und Mehrfache v-model Bindungen

Komponenten benötigen manchmal mehrere bidirektionale Bindungen. Vue unterstützt benannte v-model Bindungen, und defineModel behandelt diese durch Akzeptieren eines Namensarguments.

vue
<!-- DateRangePicker.vue -->
<script setup lang="ts">
import { computed } from 'vue'

// Benannte Models für mehrfache v-model Bindungen
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })

// Validierungs-Computed
const isValidRange = computed(() => {
  if (!startDate.value || !endDate.value) return true
  return startDate.value <= endDate.value
})
</script>

<template>
  <div class="date-range-picker">
    <input
      type="date"
      :value="startDate?.toISOString().split('T')[0]"
      @input="startDate = new Date(($event.target as HTMLInputElement).value)"
    />
    <span>bis</span>
    <input
      type="date"
      :value="endDate?.toISOString().split('T')[0]"
      @input="endDate = new Date(($event.target as HTMLInputElement).value)"
    />
    <p v-if="!isValidRange" class="error">Enddatum muss nach dem Startdatum liegen</p>
  </div>
</template>

Die Elternkomponente bindet beide Models:

vue
<DateRangePicker v-model:start="filterStart" v-model:end="filterEnd" />

v-model Modifiers mit defineModel

Vues eingebaute Modifiers wie .trim, .number und .lazy funktionieren automatisch mit defineModel. Benutzerdefinierte Modifiers erfordern explizite Behandlung durch das zweite Element des zurückgegebenen Tupels.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Zugriff auf Modifiers über den zweiten Rückgabewert
const [model, modifiers] = defineModel<number>({
  default: 0,
  // Transformationsfunktionen für benutzerdefinierte Modifiers
  set(value) {
    // Auf 2 Dezimalstellen runden, wenn 'round' Modifier vorhanden
    if (modifiers.round) {
      return Math.round(value * 100) / 100
    }
    return value
  }
})
</script>

<template>
  <div class="currency-input">
    <span>€</span>
    <input
      type="number"
      step="0.01"
      :value="model"
      @input="model = parseFloat(($event.target as HTMLInputElement).value) || 0"
    />
  </div>
</template>

Verwendung mit dem benutzerdefinierten Modifier:

vue
<CurrencyInput v-model.round="price" />

TypeScript-Muster für Script Setup

Typsicherheit in <script setup> Komponenten basiert auf Generics, die an Compiler-Makros übergeben werden. Das Muster variiert leicht zwischen Props, Emits und Models.

vue
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string }">
import { computed } from 'vue'

// Generische Komponenten-Props
const props = defineProps<{
  items: T[]
  selected?: T
}>()

const emit = defineEmits<{
  select: [item: T]
  remove: [id: string]
}>()

// Slots können ebenfalls typisiert werden
defineSlots<{
  default: (props: { item: T; index: number }) => any
  empty: () => any
}>()

const hasItems = computed(() => props.items.length > 0)
</script>

<template>
  <ul v-if="hasItems">
    <li
      v-for="(item, index) in items"
      :key="item.id"
      :class="{ selected: selected?.id === item.id }"
      @click="emit('select', item)"
    >
      <slot :item="item" :index="index">
        {{ item.id }}
      </slot>
    </li>
  </ul>
  <div v-else>
    <slot name="empty">Keine Einträge</slot>
  </div>
</template>

Generische Komponenten, verfügbar seit Vue 3.3, ermöglichen wiederverwendbare typisierte Komponenten ohne Einbußen bei der Typinferenz im konsumierenden Code.

Häufige Interview-Fragen zu Script Setup

Interviewer, die Vue-Kenntnisse testen, fokussieren sich oft auf die Unterschiede zwischen Script Setup und dem Options API sowie auf die Mechanik der Reaktivität.

Frage: Warum bricht die Destrukturierung von Props die Reaktivität und wie löst Vue 3.5 dies?

In JavaScript erstellt Destrukturierung neue Variablen, die die Werte zum Zeitpunkt der Destrukturierung halten. Wenn sich props.count später ändert, hält ein destrukturiertes const { count } = props immer noch den alten Wert. Der Compiler von Vue 3.5 transformiert die destrukturierten Variablen in Getter, die bei Zugriff neu evaluieren und somit die Reaktivität ohne Laufzeit-Overhead erhalten.

Frage: Was passiert unter der Haube, wenn defineModel verwendet wird?

Der Compiler expandiert defineModel zu einer Prop-Deklaration und einer Emit-Registrierung. Zur Laufzeit erstellt es ein Ref, dessen Getter den Prop-Wert liest und dessen Setter emit('update:modelValue', newValue) aufruft. Dieses Ref ist keine echte Kopie des Props, sondern ein Proxy, der Lese- und Schreiboperationen entsprechend weiterleitet.

Frage: Wann sollte eine Komponente Script Setup versus eine reguläre setup-Funktion verwenden?

Script Setup wird für die meisten Single File Components bevorzugt, da es Boilerplate reduziert und die IDE-Unterstützung verbessert. Eine reguläre setup() Funktion ist notwendig, wenn die Komponente Methoden über defineExpose in komplexen Mustern an Elternkomponenten exponieren muss, oder beim Erstellen von renderless Components, die Render-Funktionen direkt zurückgeben.

Migration von Options API zu Script Setup

Bestehende Codebasen enthalten oft Options API Komponenten, die von einer Migration profitieren. Die Konvertierung folgt einem vorhersagbaren Muster:

vue
<!-- Vorher: Options API -->
<script lang="ts">
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  props: {
    items: { type: Array as PropType<string[]>, required: true }
  },
  emits: ['select'],
  data() {
    return { searchTerm: '' }
  },
  computed: {
    filteredItems() {
      return this.items.filter(item =>
        item.toLowerCase().includes(this.searchTerm.toLowerCase())
      )
    }
  },
  methods: {
    selectItem(item: string) {
      this.$emit('select', item)
    }
  }
})
</script>
vue
<!-- Nachher: Script Setup -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{ items: string[] }>()
const emit = defineEmits<{ select: [item: string] }>()

const searchTerm = ref('')

const filteredItems = computed(() =>
  props.items.filter(item =>
    item.toLowerCase().includes(searchTerm.value.toLowerCase())
  )
)

function selectItem(item: string) {
  emit('select', item)
}
</script>

Die migrierte Version ist kürzer und die Typinferenz funktioniert ohne PropType Casts. Das Vue-Team stellt vue-codemod für automatisierte Migrationen bereit, obwohl eine manuelle Überprüfung für komplexe Komponenten weiterhin notwendig ist.

Performance-Überlegungen bei Script Setup

Script Setup Komponenten kompilieren zu effizienterem Code, da der Compiler die exakte Form der Komponente zur Build-Zeit kennt. Konkret bedeutet das:

  • Template-Bindungen werden zur Compile-Zeit aufgelöst statt zur Laufzeit durch Property-Lookup
  • Ungenutzte Importe werden effektiver durch Tree-Shaking entfernt
  • Das Fehlen einer umschließenden Funktion reduziert den Closure-Overhead

Für Anwendungen, die Bundle-Größe priorisieren, eliminiert Script Setup mit Vapor Mode (verfügbar ab Vue 3.6+) das virtuelle DOM vollständig für opt-in Komponenten und generiert stattdessen direkte DOM-Operationen.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Kernpunkte zu Vue 3 Script Setup und defineModel

  • Script Setup reduziert Komponenten-Boilerplate durch automatisches Exponieren von Top-Level-Bindungen an Templates und automatische Komponentenregistrierung
  • defineModel ersetzt das manuelle Prop + Emit Pattern für v-model und gibt ein Ref zurück, das mit dem gebundenen Wert der Elternkomponente synchronisiert
  • Vue 3.5 führt reaktive Props-Destrukturierung ein, die const { prop } = defineProps() ohne Verlust der Reaktivität ermöglicht
  • Benannte Models unterstützen mehrfache v-model Bindungen auf einer einzelnen Komponente durch defineModel('name')
  • TypeScript-Integration verbessert sich mit generischen Komponenten, typisierten Emits und typisierter Slot-Unterstützung durch Compiler-Makros
Tägliche Challenge

Findest du den Bug in Vue.js / Nuxt.js?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 18. September 2026

Tags

#vue
#vue3
#script-setup
#definemodel
#composition-api
#typescript

Teilen

Verwandte Artikel