statt Setup-Chaos
Die Composition API gibt Entwicklern maximale Freiheit bei der Strukturierung von Komponenten – und genau das ist das Problem. Ohne klare Konventionen wachsen setup()-Funktionen zu unlesbaren Blöcken mit hundert Zeilen vermischter Logik. Dieser Guide zeigt, wie die Vue Composition API so strukturiert wird, dass Komponenten wartbar bleiben, auch wenn sie komplex werden.
Inhaltsverzeichnis
- 1. Das Setup-Chaos-Problem: Wenn Freiheit zur Falle wird
- 2. script setup: Die moderne Composition-API-Syntax
- 3. Die Gliederungszone: Verantwortlichkeiten in setup() trennen
- 4. Composable-Extraktion: Wann setup()-Code ausgelagert wird
- 5. defineProps, defineEmits und Typisierung in script setup
- 6. Template Refs und useTemplateRef in der Composition API
- 7. defineExpose: Was öffentlich sein soll und was nicht
- 8. Große Komponenten aufbrechen: Wann Splitting Pflicht ist
- 9. Options API vs. Composition API: Strukturvergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Setup-Chaos-Problem: Wenn Freiheit zur Falle wird
Die Vue Composition API wurde als Antwort auf Skalierungsprobleme der Options API entwickelt. In der Options API wurde Logik nach Typ organisiert – alle data-Properties zusammen, alle methods zusammen, alle computed zusammen. Das Problem: Bei einer komplexen Komponente mit fünf verschiedenen Features waren alle fünf Features quer durch die Sektionen verteilt. Ein Feature zu verstehen erforderte ständiges Springen zwischen den Sektionen.
Die Composition API löst dieses Problem, indem sie erlaubt, Logik nach Feature statt nach Typ zu gruppieren. Aber ohne Konventionen entsteht ein neues Problem: Alle Freiheitsgrade werden genutzt, und das Ergebnis ist eine setup()-Funktion, in der Fetch-Logik, Form-Validierung, Event-Handling und DOM-Manipulation unstrukturiert gemischt sind. Das ist schlechter als die Options API, weil zumindest die Typen-Trennung eine Art Struktur vorgab. Setup-Chaos ist das häufigste Composition-API-Qualitätsproblem in realen Vue-3-Projekten.
Die Lösung ist nicht weniger Freiheit, sondern mehr Konvention. Klare Regeln für die Reihenfolge von Deklarationen in script setup, für die Extraktion in Composables ab einer bestimmten Komplexitätsschwelle und für die Trennung von Initialisierungs-, Reaktivitäts- und Interaktionslogik verwandeln die Freiheit der Composition API von einer Gefahr in einen Vorteil.
2. script setup: Die moderne Composition-API-Syntax
<script setup> ist die kompakteste und empfohlene Schreibweise der Vue Composition API für Single File Components. Alles, was auf der obersten Ebene des <script setup>-Blocks deklariert wird – refs, computeds, Funktionen, importierte Komponenten –, ist automatisch im Template verfügbar, ohne expliziten Return-Statement. Das eliminiert den häufigsten Boilerplate der setup()-Funktion: das Zurückgeben aller Template-Bindings.
Importierte Komponenten in <script setup> müssen nicht mehr registriert werden – sie stehen direkt im Template zur Verfügung. Das gilt auch für externe Composables und Hilfsfunktionen, die in Templates verwendet werden. defineProps() und defineEmits() sind Compiler-Macros, die Props und Emits typsicher deklarieren, ohne importiert werden zu müssen. Das Ergebnis ist weniger Boilerplate und bessere TypeScript-Integration als in der klassischen setup()-Funktion mit defineComponent().
Ein wichtiger Aspekt von <script setup>: Der Code wird einmal pro Komponentendefinition ausgeführt, nicht einmal global. Im Gegensatz zu Modulscope-Code läuft <script setup>-Code für jede Komponenteninstanz frisch. Das ist das korrekte Verhalten für reaktiven State. Wer tatsächlich nur einmal ausgeführten Initialisierungscode braucht, nutzt <script> ohne setup-Attribut in derselben Single File Component – beide Blöcke können koexistieren.
<!-- ProductCard.vue — script setup with clear structural zones -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useCartStore } from '@/stores/useCartStore'
import { useProductDetails } from '@/composables/useProductDetails'
// --- Props & Emits (always first) ---
const props = defineProps<{
productId: number
showActions?: boolean
}>()
const emit = defineEmits<{
added: [productId: number]
removed: [productId: number]
}>()
// --- Store Access ---
const cart = useCartStore()
const router = useRouter()
// --- Feature: Product Data (extracted composable) ---
const { product, loading, error } = useProductDetails(props.productId)
// --- Feature: Cart Interaction (local, simple enough to stay here) ---
const isInCart = computed(() => cart.contains(props.productId))
function addToCart() {
cart.add(props.productId)
emit('added', props.productId)
}
function goToDetail() {
router.push(`/products/${props.productId}`)
}
</script>
3. Die Gliederungszone: Verantwortlichkeiten in setup() trennen
Eine bewährte Konvention für die Strukturierung von <script setup>-Blöcken ist die Zonen-Methode: Deklarationen werden in semantisch getrennten Zonen angeordnet, die durch Kommentar-Trennlinien markiert sind. Die Reihenfolge folgt einer Logik: Was kommt von außen (Props, Emits), dann was aus dem System (Router, Stores, inject), dann reaktiver State, dann abgeleitete Werte (computed), dann Effekte (watch), dann Lifecycle-Hooks, dann Methoden. Diese Reihenfolge spiegelt den Datenfluss der Komponente wider.
Die Zonen-Methode ist keine strenge Regel, sondern eine Konvention, die Teams adaptieren können. Entscheidend ist Konsistenz: Wenn ein Entwickler zu einer Komponente navigiert, weiß er sofort, wo er Props findet, wo Computed-Properties definiert sind und wo Event-Handler stehen. Diese Vorhersagbarkeit reduziert kognitive Last beim Lesen von Code erheblich. Ein Eslint-Plugin wie eslint-plugin-vue kann bestimmte Ordnungsregeln automatisch durchsetzen und bei Abweichungen warnen.
Kommentar-Trennlinien wie // --- Feature: Pagination --- sind bei Feature-Gruppierung die natürlichere Alternative zur Typ-Reihenfolge. Bei einfachen Komponenten reicht eine Typ-Reihenfolge. Bei Komponenten mit mehreren klar getrennten Features ist Feature-Gruppierung verständlicher: Alle ref, computed, methods und watches, die zu Pagination gehören, stehen beieinander. Das macht es einfacher, ein Feature zu verstehen und zu extrahieren, wenn die Komponente zu groß wird.
4. Composable-Extraktion: Wann setup()-Code ausgelagert wird
Die wichtigste Entscheidung bei der Strukturierung der Vue Composition API ist, wann Logik in ein Composable ausgelagert wird. Drei Signale zeigen an, dass Extraktion fällig ist: Erstens, wenn eine Logikgruppe mehr als zwanzig Zeilen in setup() einnimmt und eine klare, isolierbare Verantwortlichkeit hat. Zweitens, wenn dieselbe Logikgruppe in einer anderen Komponente auch gebraucht wird. Drittens, wenn die Logikgruppe intern testenswert ist – Testbarkeit ist einfacher, wenn die Logik in einem eigenen Composable isoliert ist.
Das Extraktionsmuster ist mechanisch: Die Logikgruppe in eine neue Datei in src/composables/ verschieben, als useX-Funktion exportieren, Props und Emits, die gebraucht werden, als Parameter übergeben. In der Komponente den Composable-Aufruf mit Destructuring einsetzen. Die Komponente wird sofort lesbarer, das Composable ist isoliert testbar. Zu frühe Extraktion – bei sehr einfacher, einmaliger Logik – erzeugt unnötige Indirektion. Zu späte Extraktion lässt setup() zu einer unlesbaren Monolithfunktion wachsen.
// BEFORE: setup chaos — 60 lines of mixed concerns in one function
// src/components/ProductList.vue (problematic version)
// All of this mixed together in script setup:
// fetch logic (15 lines), pagination (12 lines),
// filter state (10 lines), sort logic (8 lines), error handling (5 lines)
// AFTER: extracted into composables — each responsible for one thing
// src/components/ProductList.vue (clean version)
import { useProductFetch } from '@/composables/useProductFetch'
import { usePagination } from '@/composables/usePagination'
import { useProductFilter } from '@/composables/useProductFilter'
import { useProductSort } from '@/composables/useProductSort'
const { products, loading, error, refetch } = useProductFetch()
const { sortedProducts } = useProductSort(products)
const { filteredProducts, activeFilter, setFilter } = useProductFilter(sortedProducts)
const { paginatedProducts, currentPage, totalPages, nextPage } = usePagination(filteredProducts)
// script setup is now 15 lines — each line is meaningful and readable
// Each composable is independently testable
// Each composable can be reused in other list components
5. defineProps, defineEmits und Typisierung in script setup
defineProps<T>() mit TypeScript-Generic ist die bevorzugte Typisierungsmethode für Props in der Composition API. Sie bietet vollständige TypeScript-Inference ohne Runtime-Overhead, weil der Vite/Vue-Compiler die Props-Typen aus dem Generic extrahiert und die Runtime-Props-Definition generiert. Der Vorteil gegenüber defineProps({ ... }) mit Runtime-Validation: TypeScript-Typen sind expressiver als Runtime-Validators und zeigen Fehler beim Compilieren statt erst beim Ausführen.
withDefaults(defineProps<T>(), { ... }) ist das Muster für Props mit Default-Werten in der typbasierten API. Es kombiniert vollständige TypeScript-Typisierung mit expliziten Default-Werten. defineEmits<T>() mit einem Interface, das Emit-Namen auf Argument-Typen-Tupel mappt – { added: [productId: number] } –, erlaubt typsichere Emit-Aufrufe, bei denen TypeScript die Argumente prüft. Das verhindert eine ganze Klasse von Bugs, die in der Options API und ohne TypeScript schwer zu finden waren: falsche Argument-Typen in Emit-Aufrufen.
6. Template Refs und useTemplateRef in der Composition API
Template Refs in der Composition API werden mit const el = ref<HTMLElement | null>(null) deklariert und via ref="el" im Template gebunden. In script setup ist der Ref-Name automatisch der Variablenname – kein String-Lookup wie in der Options API. Vue 3.5 führte useTemplateRef('name') als typsichere Alternative ein, die den DOM-Element-Typ direkt aus dem Generic-Parameter ableitet und null-safety eingebaut hat.
Das häufigste Fehlerpattern bei Template Refs in der Composition API: Zugriff auf el.value außerhalb von onMounted. Vor dem Mount ist das DOM-Element nicht verfügbar, und el.value ist null. Jeder Zugriff auf Template Refs muss entweder in onMounted, in einem Watch auf die Ref selbst mit { immediate: false }, oder mit expliziter Null-Prüfung erfolgen. Template Refs, die an Kindkomponenten gebunden sind, geben die Komponenteninstanz zurück – nur die mit defineExpose exponierten Properties sind zugänglich.
7. defineExpose: Was öffentlich sein soll und was nicht
In script setup-Komponenten ist der interne State standardmäßig privat – Elternkomponenten können via Template Ref nicht auf Properties der Kind-Komponente zugreifen, außer sie wurden explizit mit defineExpose() freigegeben. Das ist eine bewusste Designentscheidung der Composition API: Komponenten-Encapsulation ist der Default, Öffnung ist explizit. Das verhindert, dass Elternkomponenten direkten State-Zugriff auf Kinder ausüben und so enge Kopplung erzeugen.
defineExpose({ reset, validate, focus }) gibt genau die Methoden frei, die eine Elternkomponente via Ref aufrufen darf. Das ist das Pattern für wiederverwendbare Formular-Komponenten, Dialoge und Input-Komponenten, die von Eltern programmatisch gesteuert werden müssen. Die exponierten Properties sollten minimal und stabil sein – jede exponierte Property ist eine öffentliche API, die bei Änderung alle Nutzer betrifft.
<!-- InputField.vue — defineExpose for controlled parent access -->
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ label: string; modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
const inputRef = ref<HTMLInputElement | null>(null)
const hasError = ref(false)
const errorMessage = ref('')
function focus() {
inputRef.value?.focus()
}
function validate(): boolean {
// validation logic
hasError.value = !inputRef.value?.validity.valid
errorMessage.value = hasError.value ? 'Required field' : ''
return !hasError.value
}
function reset() {
hasError.value = false
errorMessage.value = ''
emit('update:modelValue', '')
}
// Only expose what parents legitimately need — keep internals private
defineExpose({ focus, validate, reset })
</script>
8. Große Komponenten aufbrechen: Wann Splitting Pflicht ist
Die Faustregel für das Aufbrechen von Komponenten in der Composition API ist einfacher als in der Options API: Wenn eine Komponente mehr als drei klar trennbare Feature-Gruppen enthält, oder wenn das Template mehr als fünfzig Zeilen hat, ist Splitting fällig. Das liegt daran, dass die Composition API Logik-Extraktion in Composables sehr einfach macht – die Barriere für das Aufbrechen ist niedrig, und die Vorteile in Lesbarkeit und Testbarkeit sind sofort. Eine Produktlistenseite, die Filter, Suche, Pagination und Produkt-Card-Rendering enthält, sollte in eine Container-Komponente mit Composables und drei bis vier Presenter-Komponenten aufgeteilt werden.
Das Container-Presenter-Muster funktioniert besonders gut mit der Composition API: Die Container-Komponente enthält alle Composable-Aufrufe und State-Verwaltung, aber wenig Template. Die Presenter-Komponenten empfangen Props und emittieren Events, haben aber wenig oder keinen eigenen State. Das macht Presenter-Komponenten trivial testbar – sie sind reine Mapping-Funktionen von Props auf Templates. Und die Container-Komponente ist durch ihre Composables ebenfalls gut testbar.
9. Options API vs. Composition API: Strukturvergleich
Der Strukturvergleich zwischen Options API und Composition API zeigt, wo die verschiedenen Ansätze ihre Stärken haben. Die Options API bietet Struktur durch erzwungene Typ-Trennung – aber diese Struktur skaliert schlecht mit Komponentenkomplexität. Die Composition API bietet Freiheit und Feature-Kohäsion – aber skaliert nur mit Konventionen gut.
| Aspekt | Options API | Composition API | Empfehlung |
|---|---|---|---|
| TypeScript-Integration | Begrenzt (this-Typing schwierig) | Exzellent | Composition API |
| Logik-Wiederverwendung | Nur via Mixins | Composables | Composition API |
| Einstiegshürde | Niedrig | Mittel (Reaktivitätsmodell) | Options API für Einsteiger |
| Skalierung bei Komplexität | Schlecht (Feature-Fragmentierung) | Gut mit Konventionen | Composition API |
| Testbarkeit der Logik | Schwierig (this-Kontext) | Einfach (Composables) | Composition API |
Die Vue Composition API ist für neue Vue-3-Projekte klar die empfohlene Wahl. Bestehende Vue-2-Projekte, die zu Vue 3 migrieren, können die Options API beibehalten oder schrittweise migrieren. Beide APIs koexistieren problemlos. Die Entscheidung für die Composition API entfaltet ihr volles Potential erst, wenn Composables konsequent eingesetzt werden und Konventionen für setup()-Struktur etabliert sind.
Mironsoft
Vue-3-Architektur, Code-Review und Composition-API-Schulung
Composition-API-Architektur für euer Team?
Wir reviewen bestehende Vue-3-Komponenten auf Setup-Chaos, etablieren Konventionen für script setup und schulen euer Team in sauberem Composition-API-Design – mit konkreten Refactoring-Beispielen aus eurem Code.
Code-Review
Setup-Chaos identifizieren, Composable-Extraktionspotential bewerten und Konventionen vorschlagen
Refactoring
Monolithische setup()-Funktionen in saubere Composable-Strukturen refaktorieren
Team-Schulung
Composition-API-Best-Practices für euer Team dokumentieren und in einem Workshop vermitteln
10. Zusammenfassung
Die Vue Composition API ist mächtiger als die Options API – aber nur, wenn ihre Freiheit durch Konventionen gebändigt wird. script setup ist die empfohlene Syntax für neue Komponenten: weniger Boilerplate, bessere TypeScript-Integration, automatisch verfügbare Template-Bindings. Die Zonen-Methode strukturiert script setup in semantisch geordnete Bereiche: Props, Emits, Stores, reaktiver State, Computed, Watch, Lifecycle, Methoden. Composable-Extraktion bei mehr als zwanzig Zeilen Logikgruppe oder bei Wiederverwendungsbedarf.
defineProps<T>() und defineEmits<T>() mit TypeScript-Generics sind die typsicheren, compiler-optimierten Deklarationsmethoden. defineExpose() öffnet explizit, was Elternkomponenten via Ref brauchen – der Rest bleibt privat. Komponenten-Splitting nach dem Container-Presenter-Muster hält Komponenten lesbar, auch wenn Features wachsen. Die Composition API entfaltet ihr volles Potential erst als Gesamtsystem: Script Setup, Composables, TypeScript und klare Konventionen zusammen.
Vue Composition API strukturieren — Das Wichtigste auf einen Blick
script setup Zonen
Props → Emits → Stores → State → Computed → Watch → Lifecycle → Methoden. Konsistente Reihenfolge für Vorhersagbarkeit.
Composable-Schwelle
Mehr als 20 Zeilen Logikgruppe, Wiederverwendungsbedarf oder Testbarkeit sind Signale für sofortige Extraktion in Composable.
TypeScript-Integration
defineProps<T>(), defineEmits<T>() mit Generics für vollständige Typ-Inferenz ohne Runtime-Overhead.
defineExpose minimal
Nur exponieren, was Eltern legitim brauchen. Jede exponierte Property ist öffentliche API. Internals privat halten.