Wiederverwendbare Logik ohne Copy-Paste
Copy-Paste zwischen Vue-Komponenten ist ein stilles Wartungsproblem: Wenn dieselbe Fetch-Logik, derselbe Validierungs-Code oder dasselbe Scroll-Verhalten in zehn Komponenten dupliziert ist, entsteht ein System, das bei jeder Bugfixierung zehnmal angefasst werden muss. Vue Composables lösen dieses Problem strukturell – durch Kapselung reaktiver Logik in useX-Funktionen, die sich wie Bausteine zusammensetzen lassen.
Inhaltsverzeichnis
- 1. Warum Vue Composables Copy-Paste strukturell verhindern
- 2. Das useX-Muster: Namenskonventionen und Dateistruktur
- 3. Reaktivität kapseln: ref, reactive und computed richtig einsetzen
- 4. Async und Fetch: useFetch als universelles Composable-Muster
- 5. Lifecycle-Hooks in Composables: onMounted, onUnmounted und watch
- 6. Composables mit Parametern: reaktive Argumente und Options-Objekte
- 7. provide/inject als Composable-Erweiterung für Komponentenbäume
- 8. Anti-Patterns: Was kein gutes Composable ist
- 9. Composables im direkten Vergleich: Mixins, Helfer und Composables
- 10. Zusammenfassung
- 11. FAQ
1. Warum Vue Composables Copy-Paste strukturell verhindern
In Vue 2 war das Haupt-Werkzeug für Logik-Wiederverwendung das Mixin. Mixins hatten ein grundsätzliches Problem: Ihre Herkunft war in der Komponente nicht sichtbar. Eine Property wie isLoading konnte aus einem Mixin stammen, aus der Komponente selbst oder durch einen Namenskonflikt aus beiden gleichzeitig – mit dem letztdefiniertem Wert als Gewinner. Vue Composables lösen dieses Problem, indem sie Logik als normale JavaScript-Funktionen kapseln, die in setup() aufgerufen werden. Der Aufrufort ist immer explizit, die Herkunft jedes Rückgabewerts ist sofort nachvollziehbar.
Ein Vue Composable ist technisch eine Funktion, die Vue-Reaktivitätsprimitive (ref, reactive, computed, watch) und optional Lifecycle-Hooks (onMounted, onUnmounted) verwendet und ihre reaktiven Zustandswerte und Methoden als Objekt zurückgibt. Entscheidend ist, dass diese Funktion nur innerhalb eines reaktiven Kontexts – also in setup(), in einem anderen Composable oder in <script setup> – aufgerufen werden darf. Außerhalb dieses Kontexts funktionieren Lifecycle-Hooks nicht, und inject() schlägt fehl. Wer das versteht, beherrscht das Fundament aller Vue Composable Patterns.
Der konkrete Nutzen zeigt sich bei der Codebasis-Analyse: Projekte, die Composables konsequent einsetzen, haben messbar weniger doppelte Logikblöcke. Ein usePagination-Composable wird einmal geschrieben und von allen Listenseiten genutzt. Ein Bug in der Seitenberechnung wird an einer Stelle gefixt – nicht in zwölf Komponenten gesucht und sieben Mal vergessen.
2. Das useX-Muster: Namenskonventionen und Dateistruktur
Die Vue-Community hat sich auf die use-Präfix-Konvention geeinigt, die aus React Hooks bekannt ist. Ein Vue Composable heißt useCounter, useFetch, useScroll – niemals counterLogic oder fetchHelper. Dieses Präfix signalisiert, dass die Funktion reaktive Primitives nutzt und nur in einem reaktiven Kontext aufgerufen werden darf. Volar (der offizielle Vue-Language-Server) erkennt das Präfix und gibt Warnungen, wenn ein Composable außerhalb von setup() aufgerufen wird.
Die Dateistruktur folgt einer einfachen Konvention: Composables liegen in src/composables/ und heißen wie ihre Exportfunktion – useFetch.ts, useAuth.ts, useMediaQuery.ts. Komplexere Domänen bekommen Unterordner: src/composables/cart/useCartItems.ts. Jede Datei exportiert genau eine Hauptfunktion als Named Export, keine Default Exports. Das erleichtert Tree-Shaking und macht Auto-Imports mit Vite/Nuxt trivial. Für domänenübergreifende Vue Composable Patterns gilt: allgemeine Composables wie useDebounce kommen in src/composables/utils/, domänenspezifische direkt in die Domäne.
Das Rückgabeobjekt eines Composables sollte immer plain sein – kein reaktives Objekt als Root, sondern einzelne ref-Werte und Methoden. Das ermöglicht Destructuring in der Komponente: const { data, loading, error } = useFetch(url). Ein Composable, das ein einzelnes reaktives Objekt zurückgibt, zwingt den Nutzer zur Verwendung von toRefs() für Destructuring – unnötige Komplexität.
// src/composables/usePagination.ts
import { ref, computed } from 'vue'
// Generic pagination composable — works with any list
export function usePagination(totalItems: number, itemsPerPage = 10) {
const currentPage = ref(1)
const perPage = ref(itemsPerPage)
const totalPages = computed(() =>
Math.ceil(totalItems / perPage.value)
)
const offset = computed(() =>
(currentPage.value - 1) * perPage.value
)
function goToPage(page: number) {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
function nextPage() { goToPage(currentPage.value + 1) }
function prevPage() { goToPage(currentPage.value - 1) }
function resetPage() { currentPage.value = 1 }
return {
currentPage,
perPage,
totalPages,
offset,
goToPage,
nextPage,
prevPage,
resetPage,
}
}
3. Reaktivität kapseln: ref, reactive und computed richtig einsetzen
Die häufigste Designfrage bei Vue Composables: ref oder reactive? Die Faustregel: ref für skalare Werte (String, Number, Boolean, Array), reactive für zusammenhängende Zustandsobjekte, bei denen man niemals das Root-Objekt ersetzen will. In der Praxis dominiert ref in Composables, weil Rückgabewerte als einzelne Refs destructured werden können, ohne Reaktivität zu verlieren. reactive-Objekte verlieren ihre Reaktivität beim Destructuring, wenn man nicht toRefs() verwendet.
computed-Properties sind in Vue Composable Patterns das Werkzeug für abgeleiteten Zustand. Sie werden nur dann neu berechnet, wenn ihre Abhängigkeiten sich ändern, und cachen das Ergebnis dazwischen. Das ist fundamental verschieden von einer einfachen Methode, die bei jedem Template-Render neu ausgeführt wird. Ein Composable, das computed für teure Berechnungen nutzt, ist automatisch performanter als eines, das Methoden zurückgibt. Writeable Computeds – mit separatem get und set – sind ein fortgeschrittenes Muster für bidirektionale Datenbindung in Composables, beispielsweise für Formular-State-Synchronisation mit einem Store.
Ein wichtiger Aspekt der Reaktivitätskapselung: Composables sollten niemals direkt auf den DOM zugreifen. Wenn ein Vue Composable mit DOM-Elementen interagieren muss – etwa für Scroll-Position, ResizeObserver oder Intersection-Detection –, empfängt es ein Ref<HTMLElement | null> als Parameter. Das Template bindet das Element via ref-Attribut und übergibt es dem Composable. So bleibt das Composable testbar ohne DOM-Umgebung.
4. Async und Fetch: useFetch als universelles Composable-Muster
Das useFetch-Composable ist das Paradebeispiel für Vue Composable Patterns, weil es zeigt, wie man Lade-, Fehler- und Erfolgszustände kapselt, die sonst in jeder Komponente dupliziert werden. Das Grundmuster: drei Refs (data, loading, error), eine async Fetch-Funktion, die diese Refs verwaltet, und ein sofortiger Aufruf in onMounted. Wer dieses Muster in einem Composable kapselt, schreibt denselben try-catch-Block nie wieder in einer Komponente.
Fortgeschrittene Vue Composable-Varianten des Fetch-Musters unterstützen reaktive URLs: Wenn die URL ein Ref<string> oder ComputedRef<string> ist, kann ein watch auf die URL reagieren und automatisch neu fetchen. Das ermöglicht Komponenten wie Produktdetailseiten, die beim Wechsel der Produkt-ID automatisch die neuen Daten laden – ohne watch in der Komponente selbst. Das Composable verwaltet den gesamten Async-Lebenszyklus intern, die Komponente sieht nur data, loading und error.
// src/composables/useFetch.ts
import { ref, watch, type Ref } from 'vue'
interface UseFetchOptions {
immediate?: boolean // fetch on mount (default: true)
initialData?: unknown // initial value for data ref
}
// Universal fetch composable — works with reactive or static URLs
export function useFetch<T>(
url: string | Ref<string>,
options: UseFetchOptions = {}
) {
const { immediate = true, initialData = null } = options
const data = ref<T | null>(initialData as T | null)
const loading = ref(false)
const error = ref<Error | null>(null)
async function execute() {
const resolvedUrl = typeof url === 'string' ? url : url.value
loading.value = true
error.value = null
try {
const response = await fetch(resolvedUrl)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json() as T
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
} finally {
loading.value = false
}
}
// Re-fetch when URL changes (only if URL is reactive)
if (typeof url !== 'string') {
watch(url, execute, { immediate })
} else if (immediate) {
execute()
}
return { data, loading, error, execute }
}
5. Lifecycle-Hooks in Composables: onMounted, onUnmounted und watch
Lifecycle-Hooks in Vue Composables registrieren sich an der aktuell aktiven Komponenteninstanz. Das bedeutet: Ein Composable, das onMounted aufruft, hängt diesen Hook an die Komponente, die das Composable aufgerufen hat. Das ist kein Fehler, sondern das beabsichtigte Design. Es ermöglicht Composables wie useEventListener, die in onMounted einen Event-Listener registrieren und in onUnmounted sauber wieder entfernen – ohne dass die Komponente selbst sich um Cleanup kümmern muss.
Das Cleanup-Muster ist eines der wichtigsten Vue Composable Patterns überhaupt. Jede Ressource, die ein Composable in onMounted anlegt – Event-Listener, Timer, WebSocket-Verbindungen, ResizeObserver –, muss in onUnmounted aufgeräumt werden. Alternativ gibt watchEffect eine Cleanup-Funktion zurück, die automatisch vor dem nächsten Effekt-Lauf und beim Unmount aufgerufen wird. Composables, die sauber aufräumen, verhindern Memory-Leaks in Single-Page-Applications, die über Stunden laufen.
watch in einem Composable sollte immer mit { immediate: false } als Standard starten, um unbeabsichtigte Initialläufe zu vermeiden. Der watchEffect dagegen läuft sofort und trackt Abhängigkeiten automatisch – er ist das richtige Werkzeug, wenn man alle reaktiven Abhängigkeiten eines Effekts automatisch verfolgen möchte, ohne sie explizit aufzuzählen. Für explizite Quellen mit klarer Logik ist watch vorzuziehen.
6. Composables mit Parametern: reaktive Argumente und Options-Objekte
Gut designte Vue Composables akzeptieren sowohl statische als auch reaktive Argumente. Die Vue-Community-Konvention dafür: Parameter als MaybeRef<T> typisieren, was T | Ref<T> entspricht, und intern toValue() (Vue 3.3+) oder unref() verwenden, um den aktuellen Wert zu extrahieren. Damit kann ein Composable mit useFetch('/api/products') genauso aufgerufen werden wie mit useFetch(productUrl), wo productUrl ein computed ist. Das macht Composables maximal flexibel.
Options-Objekte als zweites Argument sind das richtige Muster, sobald ein Vue Composable mehr als zwei Parameter hat. Statt useDebounce(value, 300, true, false) verwendet man useDebounce(value, { delay: 300, leading: true, trailing: false }). Das macht Aufruforte selbstdokumentierend und erlaubt optionale Parameter mit sinnvollen Defaults ohne Positionsabhängigkeit. TypeScript-Interfaces für Options-Objekte sollten in derselben Datei definiert und exportiert werden, damit Nutzer des Composables typsichere Konfiguration schreiben können.
7. provide/inject als Composable-Erweiterung für Komponentenbäume
Das provide/inject-Muster von Vue ist kein Ersatz für Composables, aber eine sinnvolle Erweiterung: Composables, die reaktiven Zustand über einen Komponentenbaum teilen müssen, ohne Props durchzureichen, kombinieren provide und inject mit dem Vue Composable-Muster. Das klassische Beispiel: Ein useTheme-Composable, das in einer Root-Komponente aufgerufen wird und den Theme-State mit provide(ThemeKey, { theme, toggleTheme }) bereitstellt. Child-Komponenten rufen ein zweites useTheme()-Composable auf, das intern inject(ThemeKey) aufruft.
Der Injection Key ist entscheidend für typsichere Injections. In TypeScript verwendet man InjectionKey<T> aus Vue, um den Typ des injizierten Werts zu spezifizieren: const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme'). So weiß TypeScript, welchen Typ inject(ThemeKey) zurückgibt – kein manuelles Casten nötig. Dieses Vue Composable Pattern ist die typsichere Alternative zu globalem State für Dinge wie Authentication-Context, i18n oder App-weite UI-Zustände.
// src/composables/useTheme.ts
import { ref, provide, inject, type InjectionKey } from 'vue'
type Theme = 'light' | 'dark'
interface ThemeContext { theme: ReturnType<typeof ref<Theme>>; toggle: () => void }
// Injection key with explicit type — TypeScript knows what inject() returns
const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
// Root composable: call in App.vue to provide theme
export function useThemeProvider() {
const theme = ref<Theme>('light')
const toggle = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' }
provide(ThemeKey, { theme, toggle })
return { theme, toggle }
}
// Consumer composable: call in any child component
export function useTheme() {
const ctx = inject(ThemeKey)
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider')
return ctx
}
8. Anti-Patterns: Was kein gutes Composable ist
Das häufigste Anti-Pattern bei Vue Composables ist das "God Composable": Ein einzelnes Composable, das Fetch-Logik, State-Management, Validierung und Formatierung in einer Funktion kombiniert. Solche Composables sind schwer testbar, weil alle Aspekte gemeinsam getestet werden müssen. Das Gegenrezept: Composables klein halten und zusammensetzen. Ein useProductForm ruft intern useFetch, useFormValidation und useToast auf – es koordiniert, aber implementiert nichts doppelt.
Ein zweites Anti-Pattern ist der direkte Store-Zugriff tief in Utility-Composables. Wenn useFormatPrice intern useCurrencyStore() aufruft, ist es nicht mehr testbar ohne einen vollständigen Store-Setup. Composables, die als reine Utilities konzipiert sind, sollten State als Parameter empfangen, nicht intern holen. Das Vue Composable-Design-Prinzip lautet: Utility-Composables sind zustandslos oder verwalten ihren eigenen lokalen State. Domänen-Composables dürfen auf Stores zugreifen, sind aber entsprechend schwerer isoliert testbar.
Das dritte Anti-Pattern: ein Composable, das direkt den DOM manipuliert, statt ein Template-Ref als Parameter zu empfangen. document.getElementById('header') in einem Composable bedeutet, dass das Composable nur in einer Browser-Umgebung funktioniert und nicht in SSR oder Tests ohne JSDOM. Das korrekte Muster übergibt el: Ref<HTMLElement | null> als Parameter und prüft intern auf el.value !== null vor jeder DOM-Operation.
9. Composables im direkten Vergleich: Mixins, Helfer und Composables
Der strukturelle Unterschied zwischen den drei Ansätzen für Logik-Wiederverwendung ist entscheidend für die Wahl des richtigen Vue Composable-Musters.
| Kriterium | Vue 2 Mixin | Utility-Funktion | Vue Composable |
|---|---|---|---|
| Herkunft im Template sichtbar | Nein | N/A | Ja – via Destructuring |
| Reaktivität | Implizit | Keine | Explizit und kapselbar |
| Lifecycle-Hooks | Ja, verborgen | Nein | Ja, explizit und isoliert |
| Testbarkeit | Komplex | Einfach | Gut mit mountComposable |
| Namenskonflikte | Häufig | Keine | Durch Aliase vermeidbar |
Die Tabelle verdeutlicht: Vue Composables vereinen die Stärken beider Vorgänger und eliminieren deren Schwächen. Mixins sind in Vue 3 zwar noch unterstützt, aber offiziell als Legacy-Feature markiert. Für neue Vue-3-Projekte gilt: Mixins konvertieren, sobald die Gelegenheit besteht. Utility-Funktionen bleiben sinnvoll für zustandslose, nicht-reaktive Logik – Formatierung, Berechnungen, Validatoren ohne Reactive-State.
Mironsoft
Vue-3-Architektur, Composable-Design und Frontend-Engineering
Vue-Composable-Architektur für euer Projekt?
Wir analysieren bestehende Vue-Codebases, identifizieren Copy-Paste-Muster und refaktorieren sie in sauber strukturierte Vue Composables – mit vollständiger Testabdeckung und TypeScript-Typisierung.
Composable-Audit
Bestehende Mixins und duplizierte Logik identifizieren und Migrationspfad planen
Refactoring
Mixins in typsichere Vue Composables konvertieren mit vollständigen Tests
Architektur-Review
Composable-Struktur, Naming und Composability für euer Team dokumentieren
10. Zusammenfassung
Vue Composable Patterns sind die strukturelle Antwort auf Copy-Paste in Vue-3-Projekten. Das useX-Präfix signalisiert reaktiven Kontext und erlaubt Tool-gestützte Warnungen bei Missbrauch. Einzelne ref-Werte als Rückgabewerte ermöglichen sauberes Destructuring. Async-Composables mit data, loading und error kapseln den gesamten Lebenszyklus eines API-Calls. Lifecycle-Hooks in Composables registrieren sich an der aufrufenden Komponente und ermöglichen sauberes Ressourcen-Cleanup. Das provide/inject-Muster erweitert Composables für Komponentenbaum-weiten State ohne Prop-Drilling.
Der größte Hebel liegt in der Konsistenz: Ein Projekt, das konsequent auf Vue Composables setzt, hat einen einzigen Ort für jede Logikeinheit. Neue Entwickler verstehen die Struktur sofort, weil Composables normale Funktionen sind – kein Framework-Magic, kein impliziter Merge-Algorithmus wie bei Mixins. Testbarkeit, Wartbarkeit und Erweiterbarkeit verbessern sich proportional zur konsequenten Anwendung des Composable-Musters.
Vue Composable Patterns — Das Wichtigste auf einen Blick
useX-Konvention
Immer use-Präfix, Named Export, eine Datei pro Composable. Rückgabe: einzelne Refs, keine reaktiven Root-Objekte.
Reaktivität kapseln
ref für Skalare und Arrays, computed für abgeleiteten State. MaybeRef-Parameter für maximale Flexibilität.
Lifecycle & Cleanup
onMounted/onUnmounted im Composable registrieren Hooks an der aufrufenden Komponente. Jede Ressource sauber aufräumen.
Anti-Patterns vermeiden
Keine God Composables, kein direkter DOM-Zugriff ohne Ref-Parameter, keine tief eingebetteten Store-Abhängigkeiten in Utilities.