von Composables bis zu performanten Reaktivitätsmustern
Wer Vue 3 mit denselben Patterns wie Vue 2 einsetzt, verschenkt das Potenzial der Composition API. Composables, defineModel, Suspense und reaktive Stores mit Pinia ersetzen fragile Options-API-Konstrukte durch wartbare, testbare und typsichere Vue 3 Patterns, die auch in großen Teams skalieren.
Inhaltsverzeichnis
- 1. Was Vue 3 Patterns wirklich lösen
- 2. Composition API: setup(), ref und reactive richtig einsetzen
- 3. Composables: Logik extrahieren und wiederverwenden
- 4. defineModel und Props-Patterns für saubere Komponenten-APIs
- 5. Pinia: reaktive Stores ohne Boilerplate
- 6. Performance-Patterns: computed, shallowRef und v-memo
- 7. Suspense und async Komponenten für bessere UX
- 8. Typische Fehler und wie man sie erkennt
- 9. Vue 3 Patterns im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Vue 3 Patterns wirklich lösen
Ein Vue 3 Pattern ist keine Stilentscheidung, sondern eine erprobte Lösungsstruktur für ein wiederkehrendes Problem in der Frontend-Entwicklung. Der Unterschied zu einer ad-hoc-Lösung liegt darin, dass das Muster gezielt auf Wiederverwendbarkeit, Testbarkeit und Typsicherheit ausgelegt ist. Vue 3 brachte mit der Composition API ein völlig neues Paradigma, das diese Patterns überhaupt erst ermöglicht – aber nur dann entfaltet es sein Potenzial, wenn man es konsequent anwendet statt alte Options-API-Denkmuster einfach zu übersetzen.
In der Praxis sieht man häufig, wie Teams Vue 3 einsetzen, aber weiterhin mit data(), computed: und methods: in der Options-API arbeiten, weil sie vertraut ist. Das ist technisch korrekt, aber verschenkt alles, was Vue 3 an Kompositionsmöglichkeiten mitbringt. Ein Vue 3 Pattern wie das Composable löst das Grundproblem der Options-API: Logik, die in data, computed, methods und lifecycle hooks verstreut ist, wird untrennbar an die Komponente gebunden und kann nicht ohne Weiteres wiederverwendet werden. Die folgenden Abschnitte zeigen die wichtigsten Vue 3 Patterns – vom Setup-Fundament über reaktive Stores bis hin zu Performance-Optimierungen.
2. Composition API: setup(), ref und reactive richtig einsetzen
Das Fundament aller modernen Vue 3 Patterns ist die Composition API mit ihrer setup()-Funktion, die seit Vue 3.2 durch das <script setup>-Syntaxzucker noch einfacher genutzt werden kann. Der erste wichtige Entscheid: ref versus reactive. ref wrappet jeden Wert – primitive wie Objekte – in einen reaktiven Container, der über .value zugegriffen wird. reactive macht ein Objekt direkt reaktiv, ohne .value, verliert aber die Reaktivität wenn man Eigenschaften destrukturiert. Das Vue 3 Pattern für die meisten Situationen: ref für skalare Werte und Zustand, der ausgetauscht werden soll, reactive für eng zusammengehörende Objektstrukturen wie Formulardaten.
Das <script setup>-Pattern ist in Vue 3.2+ die empfohlene Schreibweise. Alles, was im Top-Level deklariert wird, ist automatisch im Template verfügbar – kein explizites return erforderlich. defineProps und defineEmits sind Compiler-Makros, die typsichere Props und Events ohne Imports ermöglichen. Das Muster withDefaults(defineProps<Props>(), {...}) kombiniert TypeScript-Typsicherheit mit Standardwerten in einer einzigen, lesbaren Deklaration. Ein häufiger Fehler: reaktive State außerhalb von setup() oder <script setup> deklarieren – so verlieren Refs ihre Reaktivitätsverbindung zur Komponenteninstanz.
<!-- ProductCard.vue — Composition API with script setup -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { Product } from '@/types'
// Props with TypeScript types and defaults
interface Props {
product: Product
currency?: string
showStock?: boolean
}
const props = withDefaults(defineProps<Props>(), {
currency: 'EUR',
showStock: true,
})
// Emits — typed for autocomplete and runtime validation
const emit = defineEmits<{
addToCart: [productId: number, quantity: number]
wishlist: [productId: number]
}>()
// Local state — ref for primitives, reactive for form groups
const quantity = ref(1)
const isLoading = ref(false)
// Derived state — computed caches automatically
const formattedPrice = computed(() =>
new Intl.NumberFormat('de-DE', { style: 'currency', currency: props.currency })
.format(props.product.price)
)
const canAddToCart = computed(() =>
!isLoading.value && props.product.stock > 0 && quantity.value >= 1
)
// Lifecycle
onMounted(() => {
console.log('ProductCard mounted for:', props.product.id)
})
function handleAddToCart() {
if (!canAddToCart.value) return
emit('addToCart', props.product.id, quantity.value)
}
</script>
Ein weiteres wichtiges Vue 3 Pattern ist die konsequente Trennung von reaktivem Zustand und nicht-reaktiven Konstanten. Werte, die sich nie ändern, sollten mit const außerhalb der reaktiven Schicht deklariert werden – das reduziert den Overhead des Reaktivitätssystems. shallowRef und shallowReactive sind spezialisierte Varianten für große Datenstrukturen, bei denen nur die oberste Ebene reaktiv sein muss. Ein shallowRef mit einem großen Array triggert Reaktivität nur, wenn das Array selbst ausgetauscht wird – nicht bei Mutationen der Elemente. Das ist ein entscheidendes Vue 3 Pattern für Listen mit vielen Einträgen.
3. Composables: Logik extrahieren und wiederverwenden
Das wichtigste architekturelle Vue 3 Pattern ist das Composable. Ein Composable ist eine Funktion, die use als Präfix trägt, die Composition API intern nutzt und reaktiven Zustand sowie Methoden zurückgibt. Das macht Logik, die früher an eine Komponente gebunden war, vollständig wiederverwendbar – ohne Mixins, ohne Vererbung und ohne die versteckten Kollisionen von Mixin-Namespacing. Ein useFetch()-Composable kapselt Lade-Zustand, Fehlerbehandlung und das eigentliche Ergebnis in einer Funktion, die in jeder Komponente aufgerufen werden kann.
Das Vue 3 Pattern für saubere Composables: Sie sollten in sich geschlossen sein und nur das zurückgeben, was der Aufrufer braucht. Reaktiver State innerhalb eines Composables ist privat, solange er nicht explizit zurückgegeben wird. Das ermöglicht, denselben Composable mehrfach in derselben Komponente aufzurufen – jede Instanz bekommt ihren eigenen isolierten State. Im Gegensatz dazu teilen Pinia-Stores ihren State zwischen allen Konsumenten – das ist ein grundlegender Unterschied, der die Wahl zwischen Composable und Store bestimmt: lokaler Komponentenzustand gehört ins Composable, globaler Applikationszustand in den Store.
// composables/useFetch.ts — Reusable data-fetching composable
import { ref, watch, type Ref } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
isLoading: Ref<boolean>
execute: () => Promise<void>
}
export function useFetch<T>(url: Ref<string> | string): UseFetchReturn<T> {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
// Abort controller to cancel in-flight requests on URL change
let controller: AbortController | null = null
async function execute() {
const resolvedUrl = typeof url === 'string' ? url : url.value
controller?.abort()
controller = new AbortController()
isLoading.value = true
error.value = null
try {
const res = await fetch(resolvedUrl, { signal: controller.signal })
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
data.value = await res.json() as T
} catch (err) {
if ((err as Error).name !== 'AbortError') {
error.value = err as Error
}
} finally {
isLoading.value = false
}
}
// Re-fetch automatically when URL changes
if (typeof url !== 'string') {
watch(url, execute, { immediate: true })
} else {
execute()
}
return { data, error, isLoading, execute }
}
4. defineModel und Props-Patterns für saubere Komponenten-APIs
Vue 3.4 führte defineModel() als stabiles Compiler-Makro ein – eines der wichtigsten neuen Vue 3 Patterns für Formular-Komponenten. Vorher erforderte eine v-model-kompatible Komponente die explizite Deklaration eines modelValue-Props und eines update:modelValue-Emits sowie die manuelle Verkettung in computed getters und setters. defineModel() kapselt genau dieses Muster in einer einzigen Zeile und gibt ein reaktives Ref zurück, das direkt gelesen und beschrieben werden kann – Vue kümmert sich um den Props-Down-Events-Up-Zyklus im Hintergrund.
Für komplexere Komponenten-APIs ist das Vue 3 Pattern mit mehreren benannten Models besonders wertvoll. Ein Modal-Komponente kann gleichzeitig v-model:visible und v-model:activeTab exponieren – jedes mit eigenem Typ und eigenem Standardwert. Das Props-Typing-Pattern mit defineProps<Props>() kombiniert TypeScript-Interfaces mit Vue's Laufzeit-Validierung. Für Props, die entweder ein primitiver Wert oder undefined sein können, nutzt man PropType aus Vue's Typdefinitionen, um komplexe Typen wie Union-Types und generische Interfaces korrekt abzubilden.
5. Pinia: reaktive Stores ohne Boilerplate
Pinia ist der offizielle State-Manager für Vue 3 und verkörpert das moderne Vue 3 Pattern für globalen Applikationszustand. Im Gegensatz zu Vuex 4 gibt es keine Mutations, keine Namespaced-Module-Verschachtelung und kein explizites Commit-Boilerplate. Ein Pinia-Store ist konzeptionell nichts anderes als ein Composable mit persistiertem, geteiltem Zustand: state wird mit ref oder reactive definiert, getters mit computed und actions mit normalen Funktionen. Das Vue 3 Pattern für Pinia bevorzugt die Setup-Store-Variante gegenüber der Options-Store-Variante, weil sie denselben mentalen Rahmen wie <script setup> verwendet und vollständige TypeScript-Inferenz bietet.
Ein wichtiges Vue 3 Pattern für Pinia-Stores: Actions sollten die einzige Stelle sein, an der State mutiert wird. Direkte State-Mutationen aus Komponenten heraus funktionieren technisch, verletzen aber das Prinzip der nachvollziehbaren Zustandsübergänge. Mit dem Pinia-Devtools-Plugin in den Vue Devtools sieht man jeden State-Übergang mit Action-Name, Vor- und Nachzustand sowie Zeitstempel – das ist nur dann sinnvoll, wenn Mutationen über Actions laufen. Das Store-Persist-Plugin von Pinia ermöglicht, ausgewählte Store-Eigenschaften im localStorage zu speichern – ein typisches Vue 3 Pattern für User-Einstellungen wie Theme und Sprachauswahl.
// stores/cart.ts — Pinia setup store
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { CartItem, Product } from '@/types'
export const useCartStore = defineStore('cart', () => {
// State — plain refs, fully typed
const items = ref<CartItem[]>([])
const couponCode = ref<string | null>(null)
// Getters — computed, cached automatically
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const isEmpty = computed(() => items.value.length === 0)
// Actions — the only place state should be mutated
function addItem(product: Product, quantity = 1) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity += quantity
} else {
items.value.push({ ...product, quantity })
}
}
function removeItem(productId: number) {
items.value = items.value.filter(i => i.id !== productId)
}
function clearCart() {
items.value = []
couponCode.value = null
}
return { items, couponCode, totalItems, subtotal, isEmpty, addItem, removeItem, clearCart }
})
6. Performance-Patterns: computed, shallowRef und v-memo
Vue's Reaktivitätssystem ist automatisch, aber nicht magisch – falsch eingesetzte Reaktivität führt zu unnötigen Re-Renders, die sich in messbarer Trägheit der UI niederschlagen. Das grundlegende Vue 3 Pattern für Performance: alle abgeleiteten Werte in computed statt in Methoden ausdrücken. Ein computed-Wert wird nur dann neu berechnet, wenn sich seine Abhängigkeiten tatsächlich ändern und er im Template verwendet wird. Eine Methode hingegen läuft bei jedem Render-Zyklus neu durch, selbst wenn die Eingaben unverändert sind. Für große Listen ist v-memo das spezialisierte Vue 3 Pattern: es memorisiert einen Template-Teilbaum und rendert ihn nur neu, wenn sich die angegebenen Abhängigkeiten ändern.
shallowRef ist das Vue 3 Pattern für große, selten tief mutierte Datenstrukturen wie einen kompletten API-Response mit hundert Produkten. Vue verfolgt bei einem normalen ref jede verschachtelte Eigenschaft reaktiv – bei einer tiefen Struktur bedeutet das erheblichen Initialisierungsaufwand. Ein shallowRef macht nur die Referenz selbst reaktiv, nicht die Inhalte. Wenn der gesamte Array ersetzt wird, triggert die Reaktivität korrekt. Einzelne Elemente lassen sich durch triggerRef() manuell reaktiv machen, wenn es nötig ist. Das Vue 3 Pattern für Long-Lists kombiniert shallowRef mit virtualisiertem Rendering durch vue-virtual-scroller oder TanStack Virtual.
7. Suspense und async Komponenten für bessere UX
Vue 3's <Suspense>-Komponente löst ein klassisches Frontend-Problem mit einem eleganten Vue 3 Pattern: das deklarative Handling von asynchronen Komponenten-Initialisierungen. Wenn eine Komponente in ihrem setup() ein await enthält – oder einen async Composable verwendet, der ein Promise zurückgibt – kann sie von <Suspense> umschlossen werden. Solange das Promise aussteht, rendert Suspense das #fallback-Slot; sobald es aufgelöst ist, zeigt es das #default-Slot. Das ersetzt die früher übliche Pattern mit lokalen isLoading-Booleans in jeder Komponente durch einen zentralen, deklarativen Mechanismus.
Async Komponenten mit defineAsyncComponent() sind das Vue 3 Pattern für Code-Splitting auf Komponenten-Ebene. Vite und Rollup erkennen dynamische Imports automatisch und erzeugen separate Chunks für jede async Komponente. Das Ergebnis: die initiale Bundle-Größe bleibt klein, schwere Komponenten wie Rich-Text-Editoren, Chart-Libraries oder Modals werden erst geladen, wenn sie tatsächlich gerendert werden sollen. defineAsyncComponent akzeptiert ein Options-Objekt mit loadingComponent, errorComponent und delay, was das Vue 3 Pattern für professionelles Lazy-Loading komplett macht.
<!-- AsyncDashboard.vue — Suspense + defineAsyncComponent pattern -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// Heavy chart component — loaded on demand, separate bundle chunk
const SalesChart = defineAsyncComponent({
loader: () => import('@/components/SalesChart.vue'),
loadingComponent: () => import('@/components/Skeleton.vue'),
errorComponent: () => import('@/components/ErrorCard.vue'),
delay: 200, // Show loading only after 200ms (avoid flash for fast loads)
timeout: 8000, // Show error after 8s
})
</script>
<template>
<!-- Suspense handles async setup() in child components -->
<Suspense>
<template #default>
<div class="grid grid-cols-2 gap-6">
<SalesChart :period="selectedPeriod" />
<RevenueTable :limit="10" />
</div>
</template>
<template #fallback>
<div class="grid grid-cols-2 gap-6">
<SkeletonCard class="h-64" />
<SkeletonCard class="h-64" />
</div>
</template>
</Suspense>
</template>
8. Typische Fehler und wie man sie erkennt
Der häufigste Fehler in Vue 3 ist das versehentliche Verlieren der Reaktivität durch Destrukturierung. Wenn man ein reactive-Objekt destrukturiert – const { name, price } = reactive({name: 'X', price: 10}) – sind die extrahierten Eigenschaften gewöhnliche Variablen, keine reaktiven Refs mehr. Änderungen an name oder price werden vom Template nicht wahrgenommen. Das korrekte Vue 3 Pattern: toRefs() oder toRef() verwenden, wenn man Eigenschaften eines reaktiven Objekts destrukturieren und dabei die Reaktivität behalten möchte. In Composables, die ein reactive-Objekt zurückgeben, ist toRefs() beim Return-Statement Pflicht, damit der Aufrufer sicher destrukturieren kann.
Ein zweiter häufiger Fehler ist das Mutieren von Props direkt in der Kind-Komponente. Vue's Props-Down-Events-Up-Prinzip sagt: Daten fließen nach unten durch Props, Änderungen nach oben durch Emits. Wer ein Array-Prop direkt mutiert, erzeugt eine Verletzung des Datenflusses, die zu schwer nachvollziehbaren Zustandsinkonsistenzen führt. Das richtige Vue 3 Pattern: eine lokale Kopie des Props mit ref(props.items) anlegen oder defineModel() nutzen, das den Props-Update-Zyklus korrekt implementiert. Ein dritter klassischer Fehler: Watchers mit watch statt watchEffect einsetzen, wenn man alle Abhängigkeiten automatisch tracken möchte. watch erfordert explizite Source-Deklaration; watchEffect trackt automatisch alle reaktiven Werte, die im Callback gelesen werden.
9. Vue 3 Patterns im direkten Vergleich
Viele Aufgaben in Vue lassen sich auf mehrere Arten lösen – mit erheblichen Unterschieden in Wartbarkeit und Korrektheit. Die Wahl des richtigen Vue 3 Patterns hat direkte Auswirkungen auf die Langlebigkeit der Codebasis.
| Aufgabe | Options API / veraltet | Vue 3 Pattern | Vorteil |
|---|---|---|---|
| Logik teilen | Mixin mit Kollisionsgefahr | Composable (useFoo) | Isoliert, testbar, kein Namenskonflikt |
| Zwei-Wege-Binding | modelValue + emit manuell | defineModel() | Einzeiler, typsicher, Vue-intern |
| Globaler State | Vuex mit Mutations | Pinia Setup Store | Kein Boilerplate, volle TS-Inferenz |
| Async Laden | isLoading Boolean pro Komponente | Suspense + async setup | Deklarativ, zentral, wiederverwendbar |
| Code-Splitting | Alles in einem Bundle | defineAsyncComponent | Lazy-Load, kleinere Initial-Bundle |
Die Spalte "Vue 3 Pattern" zeigt nicht nur modernere Syntax – jedes der gelisteten Patterns löst ein konkretes Wartbarkeitsproblem, das die alte Variante mitbrachte. Mixins kollidierten bei gleichnamigen Eigenschaften still. Vuex-Mutations machten Typisierung umständlich. Manuelle isLoading-Flags führten zu inkonsistenter Fehlerbehandlung zwischen Komponenten. Wer die Vue 3 Patterns konsequent einsetzt, bekommt automatisch testbareren, typsichereren und wartbareren Code.
Mironsoft
Vue 3 Frontend-Entwicklung, Architektur und Performance
Vue 3 Frontend, das in großen Teams skaliert?
Wir analysieren bestehende Vue-Applikationen, identifizieren fragile Muster und migrieren sie auf bewährte Vue 3 Patterns – mit Composition API, Pinia und vollständiger TypeScript-Abdeckung.
Code-Review
Analyse auf veraltete Options-API-Muster, fehlende Typisierung und Performance-Schwachstellen
Migration
Vue 2 auf Vue 3, Vuex auf Pinia, Mixins auf Composables – sauber und inkrementell
Architektur
Komponentendesign, Store-Struktur und Composable-Bibliotheken für wachsende Teams
10. Zusammenfassung
Die wichtigsten Vue 3 Patterns für produktive Frontends lösen immer dasselbe Grundproblem: Code, der ohne klare Muster geschrieben ist, wird in großen Teams zur Blackbox. Die Composition API mit <script setup> ersetzt das verteilte Options-Objekt durch fokussierten, zusammenhängenden Code. Composables extrahieren und teilen Logik ohne Mixin-Kollisionen. defineModel() macht Zwei-Wege-Binding in Formular-Komponenten zu einem Einzeiler. Pinia's Setup-Stores bieten reaktiven globalen State ohne Vuex-Boilerplate. Suspense und async Komponenten machen Lazy-Loading und Lade-Zustände deklarativ statt imperativ.
Der größte Hebel liegt in der konsequenten Anwendung über die gesamte Codebase. Eine Komponente mit modernem Composable-Pattern neben einer anderen mit Options-API und Mixins schafft ungleiche Wartbarkeitsstandards im Team. TypeScript durch alle Schichten – Props, Emits, Composables und Stores – macht Refactoring sicher und Dokumentation teilweise überflüssig, weil die Typen selbst dokumentieren. Wer Vue 3 Patterns konsequent anwendet, bekommt eine Codebase, die mit dem Team und den Anforderungen wächst, statt unter ihnen zu brechen.
Vue 3 Patterns für produktive Frontends — Das Wichtigste auf einen Blick
Composition API
<script setup> mit ref, computed und defineProps – Pflicht in neuen Komponenten. Kein Options-API in neuem Code.
Composables statt Mixins
useFoo()-Funktionen kapseln wiederverwendbare Logik ohne Namenskollisionen, mit vollständiger TypeScript-Inferenz.
Pinia für globalen State
Setup-Store mit ref, computed und Actions – kein Vuex, keine Mutations, volle TS-Unterstützung out of the box.
Performance
shallowRef für große Daten, v-memo für Listen, defineAsyncComponent für Code-Splitting auf Komponentenebene.