<v/>
{ }
Vue 3 · Composition API · Composables · Frontend-Architektur
Vue Composable Patterns
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.

12 Min. Lesezeit ref · reactive · computed · watch · provide/inject Vue 3.x · Vite · TypeScript

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.

11. FAQ: Vue Composable Patterns

1Was ist ein Vue Composable?
Eine Funktion mit use-Präfix, die Vue-Reaktivitätsprimitive nutzt und reaktiven State sowie Methoden zurückgibt. Darf nur in setup() oder anderen Composables aufgerufen werden.
2Composable vs. Mixin?
Mixins mergen implizit, Composables geben explizit zurück. Herkunft jeder Property im Composable immer sichtbar. Keine versteckten Namenskonflikte.
3Warum ref statt reactive?
Einzelne refs können destructured werden ohne Reaktivitätsverlust. reactive-Objekte brauchen toRefs() für Destructuring – refs sind direkter.
4Lifecycle-Hooks in Composables?
onMounted/onUnmounted registrieren sich an der aufrufenden Komponente. Ermöglicht sauberes Ressourcen-Setup und Cleanup ohne Code in der Komponente selbst.
5Was ist MaybeRef?
T | Ref<T> – erlaubt statische und reaktive Argumente. intern toValue() verwenden zum Auflösen. Macht Composables maximal flexibel.
6watch vs. watchEffect?
watch: explizite Quellen, kein sofortiger Lauf per Default. watchEffect: automatisches Tracking, läuft sofort. watch für klare Quelle-Effekt-Beziehungen bevorzugen.
7Composables in Composables?
Ja – Kernmuster der Komposition. useProductForm ruft intern useFetch, useFormValidation, useToast auf. Komplexe Workflows aus kleinen Bausteinen zusammensetzen.
8Wie teste ich Composables?
Mit @vue/test-utils in minimalem Wrapper oder mountComposable. Ohne Lifecycle-Hooks: withSetup im reaktiven Kontext. Stores mocken für Store-abhängige Composables.
9provide/inject vs. Store?
provide/inject für State in einem Komponentenbaum. Pinia für globalen, anwendungsweiten State. Composables können beides kombinieren.
10Was ist das God-Composable-Anti-Pattern?
Ein Composable, das Fetch, Validierung, Formatierung und State kombiniert. Kaum testbar, verletzt Single Responsibility. Lösung: kleine Composables schreiben und koordinierend zusammensetzen.