Deine ersten Vue-3-Composables
Lerne, wiederverwendbare Logik aus Komponenten herauszulösen: vom einfachen Zähler bis zu einem useFetch, das veraltete Anfragen abbricht.

Quest-Ziele
- Verstehen, was eine Funktion zu einem Composable macht
- Zustandsbehaftete Logik aus einer Komponente herauslösen
- Ein Ref mit localStorage synchron halten
- Ein useFetch schreiben, das veraltete Anfragen abbricht
Komponenten eignen sich hervorragend, um Markup wiederzuverwenden, aber früher oder später möchtest du auch Logik wiederverwenden: einen Timer, die Validierung eines Formulars, einen Datenabruf. In Vue 3 ist das die Aufgabe eines Composables. Ein Composable ist eine einfache Funktion, die die Composition API nutzt und reaktiven Zustand zurückgibt. Sein Name beginnt üblicherweise mit use.
Los geht's mit einer Komponente
Hier ist eine Komponente mit ein wenig Logik:
<script setup lang="ts">
import { computed, ref } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value += 1;
}
</script>
<template>
<button @click="increment">{{ count }} (double: {{ double }})</button>
</template>Angenommen, eine zweite Komponente braucht denselben Zähler. Statt ihn zu kopieren, verschiebst du die Logik in eine eigene Datei. Ändern musst du dabei nichts, denn ref und computed funktionieren auch außerhalb von Komponenten:
import { computed, ref } from 'vue';
export function useCounter(initial = 0) {
const count = ref(initial);
const double = computed(() => count.value * 2);
function increment() {
count.value += 1;
}
return { count, double, increment };
}Die Komponente wird deutlich kleiner. Sie sagt nur noch, was sie braucht:
<script setup lang="ts">
import { useCounter } from '../composables/useCounter';
const { count, double, increment } = useCounter(10);
</script>
<template>
<button @click="increment">{{ count }} (double: {{ double }})</button>
</template>Die Verbindung zur Außenwelt
Richtig nützlich werden Composables, wenn sie etwas kapseln, das von Natur aus nicht reaktiv ist. localStorage ist ein gutes Beispiel: Dieses Ref merkt sich seinen Wert über das Neuladen der Seite hinweg.
import { ref, watch, type Ref } from 'vue';
export function useStoredRef<T>(key: string, initial: T): Ref<T> {
const stored = localStorage.getItem(key);
const value = ref(stored === null ? initial : JSON.parse(stored)) as Ref<T>;
watch(
value,
(next) => localStorage.setItem(key, JSON.stringify(next)),
{ deep: true },
);
return value;
}Du verwendest es wie ein normales Ref, zum Beispiel mit const theme = useStoredRef('theme', 'dark'). Jede Änderung wird automatisch gespeichert, und { deep: true } erfasst auch Änderungen in Objekten und Arrays.
Ein useFetch, das hinter sich aufräumt
Ein klassischer Fehler: Jemand klickt schnell, zwei Anfragen starten, und die langsamere erste Antwort überschreibt die neuere. Die Lösung ist, die alte Anfrage abzubrechen, sobald sich die URL ändert.
watchEffect läuft erneut, sobald sich ein gelesener reaktiver Wert ändert. onWatcherCleanup(), verfügbar seit Vue 3.5, registriert eine Funktion, die vor dem nächsten Durchlauf ausgeführt wird. Das ist die perfekte Stelle zum Abbrechen. toValue() erlaubt dem Composable, einen einfachen String, ein Ref oder einen Getter anzunehmen.
import {
onWatcherCleanup,
ref,
shallowRef,
toValue,
watchEffect,
type MaybeRefOrGetter,
} from 'vue';
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = shallowRef<T | null>(null);
const error = shallowRef<Error | null>(null);
const loading = ref(false);
async function load(target: string, signal: AbortSignal) {
loading.value = true;
error.value = null;
try {
const response = await fetch(target, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = (await response.json()) as T;
loading.value = false;
} catch (caught) {
// A newer request took over, and it is the one that ends the loading state.
if (signal.aborted) return;
error.value = caught as Error;
loading.value = false;
}
}
watchEffect(() => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
load(toValue(url), controller.signal);
});
return { data, error, loading };
}Übergib einen Getter, und das Composable lädt neu, sobald sich ein Wert darin ändert:
<script setup lang="ts">
import { ref } from 'vue';
import { useFetch } from '../composables/useFetch';
type User = { id: number; name: string };
const id = ref(1);
const { data: user, error, loading } = useFetch<User>(
() => `https://jsonplaceholder.typicode.com/users/${id.value}`,
);
</script>
<template>
<button @click="id += 1">Next user</button>
<p v-if="loading">Loading...</p>
<p v-else-if="error">{{ error.message }}</p>
<p v-else-if="user">{{ user.name }}</p>
</template>Drei Faustregeln
Rufe Composables ganz oben in
<script setup>auf, nicht in Callbacks oder Bedingungen, damit Vue sie der aktuellen Komponente zuordnen kann.Gib einfache Objekte mit Refs zurück. So bleiben sie beim Destrukturieren reaktiv.
Räume auf, was du startest: Timer, Event-Listener und Anfragen.