Modul 4: Composition API

Composables schreiben

Eigene Composables erstellen – Vues mächtigstes Werkzeug für wiederverwendbare Logik.

Was ist ein Composable?

Ein Composable ist eine Funktion, die Vues Composition API nutzt, um reaktiven State und Logik zu kapseln. Der Name kommt von „compose" – du komponierst deine Komponenten-Logik aus kleinen, wiederverwendbaren Bausteinen.

Technisch gesehen ist ein Composable einfach eine Funktion, die:

  • Reaktiven State erstellt (ref, reactive, computed)
  • Lifecycle-Hooks nutzen kann (onMounted, onUnmounted)
  • State und Methoden als Objekt zurückgibt
  • In jeder Komponente per Import genutzt werden kann
🛤️

Rails-Vergleich

Composables sind wie Service Objects oder POROs in Rails – eigenständige Klassen/Module, die eine bestimmte Aufgabe kapseln. Der Unterschied: Composables sind automatisch reaktiv. Stell dir ein Service Object vor, das sich selbst aktualisiert, wenn sich seine Eingabedaten ändern.

Namenskonvention: useXxx

Per Konvention beginnen Composables immer mit use – z.B. useCounter, useFetch, useAuth. Das hat mehrere Vorteile:

  • Sofort als Composable erkennbar
  • IDE-Autocomplete: Tippe use und sieh alle verfügbaren Composables
  • In Nuxt werden composables/-Dateien mit use-Prefix automatisch importiert
  • Konsistent mit React Hooks und der VueUse-Bibliothek
Dateistruktur
composables/
├── useAuth.ts          # Authentifizierung
├── useCounter.ts       # Zähler-Logik
├── useDarkMode.ts      # Theme-Umschaltung
├── useFetch.ts         # API-Aufrufe
└── useMousePosition.ts # Maus-Tracking
💡

Nuxt Auto-Import

In Nuxt 3 werden alle Dateien im composables/-Verzeichnis automatisch importiert. Du brauchst kein import-Statement – schreib einfach useCounter() in deiner Komponente.

Dein erstes Composable: useCounter

Beginnen wir mit einem einfachen Beispiel. Dieses Composable kapselt einen Zähler mit Increment, Decrement und Reset:

composables/useCounter.ts
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  function increment() {
    count.value++
  }
  function decrement() {
    count.value--
  }
  function reset() {
    count.value = initialValue
  }
  return {
    count: readonly(count),  // Nur lesen von außen
    increment,
    decrement,
    reset
  }
}

Beachte die Struktur: Die Funktion erstellt reaktiven State, definiert Methoden, und gibt alles als Objekt zurück. Jeder Aufruf von useCounter() erstellt eine eigene Instanz mit eigenem State.

Composables in Komponenten nutzen

Die Nutzung ist denkbar einfach – Funktion aufrufen und die Rückgabewerte destrukturieren:

components/MyCounter.vue
<template>
  <div>
    <p>Zähler: {{ count }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">−</button>
    <button @click="reset">Reset</button>
  </div>
</template>
<script setup>
// In Nuxt: Kein Import nötig (auto-imported aus composables/)
const { count, increment, decrement, reset } = useCounter(10)
</script>
ℹ️

Destrukturierung und Reaktivität

Da count ein ref ist, bleibt die Reaktivität beim Destrukturieren erhalten. Bei reactive-Objekten wäre das nicht der Fall – deshalb geben die meisten Composables ref-Werte zurück.

Reaktiven State zurückgeben

Es gibt verschiedene Patterns, wie ein Composable seinen State zurückgeben kann:

Return-Patterns
// Option 1: Einzelne Refs zurückgeben (empfohlen)
function useCounter() {
  const count = ref(0)
  const doubled = computed(() => count.value * 2)
  return { count, doubled }
}
// Nutzung: const { count, doubled } = useCounter()
// Option 2: Reactive-Objekt zurückgeben
function useCounter() {
  const state = reactive({ count: 0 })
  return toRefs(state)  // In Refs konvertieren!
}
// Option 3: Readonly für geschützten State
function useCounter() {
  const count = ref(0)
  return {
    count: readonly(count),  // Von außen nicht änderbar
    increment: () => count.value++
  }
}
💡

Empfehlung

Nutze Option 1 (einzelne Refs) als Standard. Das ist am flexibelsten – Konsumenten können genau das importieren, was sie brauchen, und Refs behalten ihre Reaktivität beim Destrukturieren.

Praxis: useMousePosition

Ein realistischeres Beispiel: Ein Composable, das die Mausposition verfolgt. Hier siehst du auch Lifecycle-Hooks in einem Composable – die Event-Listener werden beim Mounten registriert und beim Unmounten entfernt:

composables/useMousePosition.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}
// Nutzung in einer Komponente:
// const { x, y } = useMousePosition()
useMousePosition Demo
Interaktiv
Bewege die Maus hierhin
ℹ️

Cleanup ist entscheidend

Der onUnmounted-Hook entfernt den Event-Listener. Ohne Cleanup würde bei jedem Mounten/Unmounten ein neuer Listener dazukommen – ein klassisches Memory Leak. In Rails passiert das Äquivalent bei WebSocket-Subscriptions in Action Cable, die nicht sauber abgemeldet werden.

Composables mit Parametern

Composables können Parameter entgegennehmen, um ihr Verhalten zu konfigurieren. Akzeptiere sowohl einfache Werte als auch Refs – das macht das Composable flexibler:

composables/useTitle.ts
import { ref, watch, type MaybeRef } from 'vue'
export function useTitle(newTitle: MaybeRef<string>) {
  const title = ref(toValue(newTitle))
  // Wenn ein Ref übergeben wird, reagiere auf Änderungen
  watch(
    () => toValue(newTitle),
    (val) => {
      title.value = val
      if (false) {
        document.title = val
      }
    },
    { immediate: true }
  )
  return title
}
// Nutzung:
// useTitle('Meine Seite')                    // Statisch
// useTitle(computed(() => user.value.name))   // Reaktiv
💡

MaybeRef Pattern

Der Typ MaybeRef<T> akzeptiert sowohl T als auch Ref<T>. Mit toValue() bekommst du immer den einfachen Wert heraus. So kann der Konsument entscheiden, ob der Parameter reaktiv sein soll oder nicht.

Best Practices

Immer mit use beginnen

useAuth, useDarkMode, useApi – konsistent und sofort erkennbar.

Refs statt Reactive zurückgeben

Refs behalten Reaktivität beim Destrukturieren – reactive nicht.

Cleanup in onUnmounted

Listener, Timer und Subscriptions immer aufräumen.

Composables nicht in Schleifen oder Conditions aufrufen

Immer auf Top-Level von setup aufrufen – wie React Hooks.

Kein globaler State ohne Absicht

State innerhalb der Funktion = pro Instanz. State außerhalb = geteilt (singleton).

Zusammenfassung

Composables sind Vues Antwort auf die Frage: „Wie teile ich Logik zwischen Komponenten?" Sie sind einfache Funktionen, die reaktiven State kapseln und zurückgeben. Mit der use-Konvention, sauberem Cleanup und Ref-basierten Rückgabewerten baust du eine Bibliothek wiederverwendbarer Bausteine, die dein Projekt skalierbar halten. Im nächsten Kapitel schauen wir uns fortgeschrittene Composable-Patterns an.