
Vue 3 Composables werden oft als Ersatz für Mixins oder als bequeme Möglichkeit zum Teilen von Logik zwischen Komponenten vorgestellt. Obwohl beide Aussagen technisch korrekt sind, erfassen sie nicht, worin Composables wirklich gut sind. In der Praxis sind Composables ein architektonisches Werkzeug zur Modellierung reaktiven Verhaltens über die Zeit. Sie helfen dabei zu definieren, wo Reaktivität lebt, wie sie fließt und welche Teile des Systems für Veränderungen verantwortlich sind.
Composables auf dieser Ebene zu verstehen erfordert einen Perspektivwechsel. Anstatt zu fragen „Kann diese Logik wiederverwendet werden?“, sollte man fragen: „Beschreibt diese Logik ein Verhalten, das sich über die Zeit entwickelt und auf Änderungen reagiert?“. Wenn ja, ist ein Composable in der Regel die richtige Abstraktion. Diese Betrachtungsweise führt zu einer klareren Architektur, bei der Composables nicht primär der Wiederverwendung dienen, sondern der expliziten Modellierung reaktiver Abhängigkeiten und von Logik, die sich über die Zeit entwickelt.
Von komponentenzentriertem zu verhaltenszentriertem Design
Vor der Composition API waren Vue-Komponenten die primäre Einheit für Verhalten. State, Effekte und abgeleitete Werte tendierten dazu, sich innerhalb von Komponenten anzusammeln, oft gruppiert nach Lifecycle-Hooks statt nach Absicht. Mixins versuchten, Wiederverwendung zu adressieren, verwischten aber die Zuständigkeiten und machten Abhängigkeiten implizit.
Composables kehren diese Beziehung um. Eine Komponente wird zum Konsumenten und Koordinator von Verhalten, während Composables zum Ort werden, an dem Verhalten definiert wird. Diese Verschiebung ist subtil, aber wichtig. Anstatt zu denken “diese Komponente braucht X und Y”, entsteht eine Perspektive, bei der Komponenten aus klar definierten Verhaltenseinheiten zusammengesetzt werden. Deshalb sind Composables auch dann wertvoll, wenn sie nur einmal verwendet werden. Wenn das Extrahieren von Logik in ein Composable die Komponente wie eine übergeordnete Beschreibung statt wie ein Implementierungsdetail lesen lässt, hat es sich bereits ausgezahlt.
Was etwas grundlegend zu einem Composable macht
Auf technischer Ebene ist ein Composable einfach eine Funktion, die Vue Reaktivitäts-API verwendet. Konzeptionell repräsentiert ein Composable jedoch Logik, die eine Dauer hat. Es ist Logik, die startet, reagiert und schließlich stoppt.
Der Unterschied zu einfachen Funktionen ist fundamental: Eine einfache Funktion nimmt Input, produziert Output und beendet ihre Ausführung. Jeder Aufruf ist isoliert und unabhängig. Ein Composable hingegen etabliert Beziehungen und reaktive Abhängigkeiten, die so lange bestehen, wie die Komponenteninstanz existiert. Es verwaltet State über die Zeit und reagiert kontinuierlich auf Änderungen.
Diese Unterscheidung erklärt, warum manche Abstraktionen sich unbeholfen anfühlen, wenn sie in Composables gezwungen werden. Logik ohne zeitliche Dimension, ohne sich entwickelnden State und ohne Reaktion auf Veränderung profitiert nicht von Reaktivität. Hier sind einfache Funktionen die bessere Wahl.
Reaktivität ist nicht implizit – sie muss gestaltet werden
Einer der häufigsten Fehler beim Schreiben von Composables ist die Annahme, dass Reaktivität “einfach funktioniert”. Vue Reaktivitätssystem ist mächtig, aber auch explizit. Ein Wert ist nur dann reaktiv, wenn er getrackt wird, und er wird nur getrackt, wenn er in einem reaktiven Kontext zugegriffen wird. Dies wird besonders wichtig an Composable-Grenzen.
Bei einem Composable, das von externem Input abhängt, ist die erste Frage, ob dieser Input Konfiguration oder eine reaktive Abhängigkeit darstellt. Konfiguration kann sicher als statisch behandelt werden. Eine reaktive Abhängigkeit muss das Composable explizit beobachten. Die Funktionssignatur sollte diese Entscheidung widerspiegeln.
Statische und reaktive Inputs sind unterschiedliche Verträge
Ein Composable, das einen statischen Input akzeptiert, signalisiert, dass dieser Wert sowie alle darauf basierenden Berechnungen nur einmal ausgewertet werden und sich über die gesamte Lebensdauer der Komponente nicht ändern. Dies ist ein vollkommen legitimer Anwendungsfall:
export function usePagination(pageSize: number) {
const page = ref(1)
const next = () => page.value + 1
const offset = computed(() => (page.value - 1) * pageSize)
return { page, next, offset }
}Code-Sprache: JavaScript (javascript)Hier ist pageSize Konfiguration. Es reaktiv zu machen würde Komplexität ohne Nutzen hinzufügen.
Im Kontrast dazu steht ein Composable, das von etwas abhängt, das sich über die Zeit ändern kann, wie etwa einem Identifier oder Filter.
export async function useUser(userId: string) {
const user = ref<User | null>(null)
const data = await fetchUser(userId)
user.value = data
return { user }
}Code-Sprache: JavaScript (javascript)Diese Implementierung funktioniert nur so lange, wie sich userId nie ändert. Falls doch, wird das Composable stillschweigend inkorrekt. Das Problem ist nicht, dass der Code falsch ist, sondern dass die API ein Versprechen macht, das sie nicht halten kann.
MaybeRefOrGetter als expliziter Reaktivitätsvertrag
Vue Typ MaybeRefOrGetter<T> wurde in der Version 3.3 genau dafür eingeführt, diese Mehrdeutigkeit auf sichere Weise zu modellieren. Er erlaubt es einem Composable, reaktive und nicht-reaktive Inputs gleichermaßen zu akzeptieren und entsprechend zu verarbeiten. Diesen Typ zu verwenden geht nicht um Bequemlichkeit, sondern um Ehrlichkeit.
export function useUser(userId: MaybeRefOrGetter<string>) {
const user = ref<User | null>(null)
watch(
() => toValue(userId),
async id => {
user.value = await fetchUser(id)
},
{ immediate: true }
)
return { user }
}Code-Sprache: JavaScript (javascript)Das Composable hat jetzt einen klaren Verhaltensvertrag. Bei einem einfachen String wird der User einmal abgerufen. Bei einer Ref oder einem Computed-Getter wird der User neu abgerufen, wann immer sich der Wert ändert. Das Composable muss nicht wissen, mit welchem Fall es zu tun hat – toValue normalisiert den Input. Dieses Muster erlaubt es Composables, flexibel zu bleiben, ohne vage zu werden.
Reaktivität innerhalb des Composables bewahren
Reaktive Inputs zu akzeptieren ist nur die halbe Geschichte. Das Composable muss auch sicherstellen, dass Reaktivität intern bewahrt wird. Der häufigste Weg, Reaktivität zu brechen, ist das zu frühe Auslesen reaktiver Werte.
Dies ist reaktiv:
const fullName = computed(() => `${toValue(firstName)} ${toValue(lastName)}`)Code-Sprache: JavaScript (javascript)Dies ist es nicht:
const firstNameValue = toValue(firstName)
const fullName = computed(() => `${firstNameValue} ${toValue(lastName)}`)Code-Sprache: JavaScript (javascript)Der Unterschied liegt darin, wo der Wert zugegriffen wird. Vue trackt reaktive Abhängigkeiten nur, wenn sie innerhalb eines reaktiven Kontexts wie watch, computed oder watchEffect zugegriffen werden. Einen Wert während des Setups auszulesen friert ihn in der Zeit ein – firstNameValue wird nie aktualisiert, selbst wenn sich firstName ändert.
Das gleiche Prinzip gilt für alle abgeleiteten States. Berechnete Werte und Watcher sollten reaktive Quellen in ihren Getter-Funktionen oder Callbacks zugreifen, nicht auf Snapshots, die außerhalb erstellt wurden.
Composables testen
Die Testbarkeit eines Composables ist ein direkter Indikator für sein Design. Ein gut strukturiertes Composable lässt sich isoliert testen, ohne eine komplette Vue-Komponente aufsetzen zu müssen. Vue bietet dafür die @vue/test-utils Bibliothek, aber oft reicht auch ein einfacherer Ansatz.
Für rein synchrone Composables ohne Lifecycle-Abhängigkeiten kann ein Test direkt die reaktiven Werte beobachten:
describe('usePagination', () => {
it('berechnet offset korrekt', () => {
const { page, offset } = usePagination(10)
expect(offset.value).toBe(0)
page.value = 2
expect(offset.value).toBe(10)
page.value = 5
expect(offset.value).toBe(40)
})
})Code-Sprache: PHP (php)Dieser Test benötigt keine spezielle Test-Umgebung. Das Composable wird aufgerufen, reaktive Werte werden manipuliert, und Ergebnisse werden geprüft. Die Einfachheit dieses Tests deutet auf gutes Design hin.
Für Composables mit asynchronem Verhalten oder Lifecycle-Hooks wird @vue/test-utils benötigt, um einen Komponenten-Kontext zu simulieren:
vi.mock('./api', () => ({
fetchUser: vi.fn((id) => Promise.resolve({ id, name: `User ${id}` }))
}))
describe('useUser', () => {
it('lädt User bei Änderung der ID neu', async () => {
const userId = ref('1')
let composableResult
const wrapper = shallowMount({
setup() {
composableResult = useUser(userId)
return {}
},
template: '<div></div>'
})
await nextTick()
expect(composableResult.user.value).toEqual({ id: '1', name: 'User 1' })
userId.value = '2'
await nextTick()
expect(composableResult.user.value).toEqual({ id: '2', name: 'User 2' })
})
})Code-Sprache: JavaScript (javascript)Ein weiteres Testmuster ist das Testen der Reaktivität selbst:
export function useFilteredData(items: MaybeRefOrGetter<Item[]>) {
const filter = ref('')
const filtered = computed(() => {
return toValue(items).filter(i => i.name.includes(filter.value))
})
return { filter, filtered }
}Code-Sprache: JavaScript (javascript)describe('useFilteredData', () => {
it('reagiert auf Änderungen in items und filter', async () => {
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Apricot' }
])
const { filter, filtered } = useFilteredData(items)
expect(filtered.value).toHaveLength(3)
filter.value = 'Ap'
await nextTick()
expect(filtered.value).toHaveLength(2)
expect(filtered.value.map(i => i.name)).toEqual(['Apple', 'Apricot'])
items.value.push({ id: 4, name: 'Applesauce' })
await nextTick()
expect(filtered.value).toHaveLength(3)
})
})Code-Sprache: JavaScript (javascript)Dieser Test verifiziert, dass das Composable korrekt auf Änderungen sowohl im Filter als auch in der Item-Liste reagiert. Das ist genau die Art von Verhalten, die Composables modellieren sollen.
Tests sind auch eine Form der Dokumentation. Ein gut geschriebener Test zeigt, wie das Composable verwendet werden soll, welche Inputs es akzeptiert und welches Verhalten erwartet wird.
Komposition von Composables
Composables werden oft nicht isoliert verwendet, sondern bauen aufeinander auf. Ein Composable kann andere Composables aufrufen und deren Outputs als eigene reaktive Quellen verwenden. Diese Komposition ist einer der größten Vorteile der Composition API.
export function useUserPreferences(userId: MaybeRefOrGetter<string>) {
const { user } = useUser(userId)
const preferences = computed(() => user.value?.preferences ?? defaultPreferences)
const theme = computed(() => preferences.value.theme)
const locale = computed(() => preferences.value.locale)
return { preferences, theme, locale }
}Code-Sprache: JavaScript (javascript)Wichtig ist, dass jede Ebene der Komposition reaktiv bleibt. Das innere useUser Composable gibt eine reaktive user Ref zurück. Das äußere Composable baut darauf auf, ohne die Reaktivität zu brechen. Änderungen an userId propagieren durch useUser zu user, dann zu preferences, und schließlich zu theme und locale.
Diese Art der Komposition funktioniert, weil jedes Composable seine eigenen reaktiven Primitive korrekt erstellt und zurückgibt. Es gibt keine versteckten Seiteneffekte oder globalen Zustände, die die Komposition komplizieren würden.
Lifecycle-Bewusstsein als Teil der Composable-Verantwortung
Da Composables während des Komponenten-Setups laufen, nehmen sie natürlich am Komponenten-Lifecycle teil. Das ist eine Stärke, aber auch eine Quelle subtiler Bugs.
Vue räumt vieles automatisch auf: computed, watchund watchEffect, die im Setup-Kontext erstellt werden, sind an den Effect Scope der Komponente gebunden und werden beim Unmount gestoppt. Solange ein Composable nur mit Vues Reaktivitätsprimitiven arbeitet, muss man sich um Cleanup keine Gedanken machen.
Sobald ein Composable aber Ressourcen außerhalb des Reaktivitätssystems anlegt – Event-Listener auf windowoder document, Timer, Observer, externe Subscriptions – endet diese Garantie. Diese Seiteneffekte muss das Composable selbst paaren: einmal beim Mount erstellen, beim Unmount wieder abbauen. Andernfalls leckt es Verhalten über die Lebensdauer der Komponente hinaus.
export function useWindowWidth() {
const width = ref(window.innerWidth)
const update = () => {
width.value = window.innerWidth
}
onMounted(() => {
window.addEventListener('resize', update)
})
onUnmounted(() => {
window.removeEventListener('resize', update)
})
return { width }
}Code-Sprache: JavaScript (javascript)window.addEventListener ist ein klassischer Fall: eine Browser-API, von der Vue nichts weiß. Die Verantwortung für das Aufräumen liegt beim Composable, nicht beim Konsumenten – und genau das macht das Beispiel oben.
Lokaler versus geteilter State in Composables
Standardmäßig erstellt jeder Aufruf eines Composables eine neue Instanz von State. Dies spiegelt wider, wie Komponenten funktionieren, und ist normalerweise das gewünschte Verhalten. Manchmal definiert ein Composable jedoch absichtlich State auf Modul-Ebene, um ihn zwischen Konsumenten zu teilen. Dies verwandelt das Composable effektiv in einen leichtgewichtigen Store.
Das ist nicht von Natur aus falsch, ändert aber die Semantik der Abstraktion. Das Composable beschreibt nicht länger Verhalten, das an eine Komponenteninstanz gebunden ist, sondern Verhalten, das an die Anwendung gebunden ist. Geteilter State sollte eine explizite Design-Entscheidung sein, kein versehentlicher Nebeneffekt davon, wo eine Ref zufällig deklariert wurde.
TypeScript als Teil des Design-Prozesses
TypeScript ist nicht nur ein Sicherheitsnetz für Composables; es ist ein Design-Werkzeug. Die Typen, die gewählt werden, kommunizieren Absicht an Konsumenten und dokumentieren das erwartete Verhalten.
Ein Argument als MaybeRefOrGetter<T> zu typen kommuniziert, dass Reaktivität unterstützt und erwartet wird. Es als T zu typen kommuniziert, dass es Konfiguration ist. Refs zurückzugeben kommuniziert, dass Werte beobachtet werden sollen. Einfache Werte zurückzugeben kommuniziert Endgültigkeit.
Ein Composable mit einer gut gestalteten Typsignatur benötigt oft weniger Dokumentation, weil sein Verhalten in seiner Form kodiert ist.
Wann ein Composable nicht die richtige Abstraktion ist
Trotz ihrer Flexibilität sind Composables nicht universell angemessen. Logik, die rein rechnerisch, synchron und zustandslos ist, wird normalerweise besser als einfache Funktion ausgedrückt. Solche Logik in ein Composable zu extrahieren verschleiert oft ihre Einfachheit und führt unnötige Kopplung an Vue ein.
Ebenso profitiert Logik, die tief spezifisch für eine einzelne Komponente ist und sich wahrscheinlich nicht unabhängig weiterentwickeln wird, möglicherweise nicht von Extraktion. Abstraktion ist nur wertvoll, wenn sie die kognitive Last reduziert. Wenn sie Komplexität nur verlagert, ist sie wahrscheinlich verfrüht.
Fazit zu Vue 3 Composables
Composables sind eine der mächtigsten Ideen in Vue 3, nicht weil sie Wiederverwendung ermöglichen, sondern weil sie bewusstes Nachdenken über Reaktivität fördern. Sie erzwingen Entscheidungen darüber, was reaktiv ist, was statisch ist, was State besitzt und wie lange Verhalten leben sollte.
Werkzeuge wie MaybeRefOrGetter bedacht einzusetzen erlaubt es Composables, flexibel zu bleiben, ohne mehrdeutig zu werden. Reaktivität intern zu bewahren stellt sicher, dass sie sich über die Zeit vorhersagbar verhalten. Zusammen verwandeln diese Praktiken Composables in verlässliche Bausteine statt in beiläufige Helfer.
In gut strukturierten Vue-Codebasen verschwinden Composables oft im Hintergrund. Sie sind nicht auffällig, aber sie erzwingen stillschweigend Konsistenz und Klarheit. Diese Unsichtbarkeit ist normalerweise ein Zeichen dafür, dass sie mit Absicht statt aus Gewohnheit gestaltet wurden.
Food for your brain!
Du möchtest noch mehr Beiträge rund um IT, KI und digitale Transformation lesen?
Dann melde dich zu unserem Contentletter an. Er erscheint quartalsweise, bleibt angenehm kompakt und bringt dir die wichtigsten Impulse direkt ins Postfach, ganz ohne E-Mail-Flut.
Stay connected!
Was uns gerade bewegt, woran wir arbeiten und welche Themen relevant
werden, teilen wir auf LinkedIn.


