Composables
Composables sind Funktionen, die die Composition API nutzen, um wiederverwendbare Logik zu kapseln und zwischen Komponenten zu teilen.
Sie sind das Vue 3 Äquivalent zu Mixins - aber besser strukturiert und typsicher.
Warum Composables?
Abschnitt betitelt „Warum Composables?“<script setup>import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)const y = ref(0)
function update(e) { x.value = e.pageX y.value = e.pageY}
onMounted(() => window.addEventListener('mousemove', update))onUnmounted(() => window.removeEventListener('mousemove', update))</script>Code muss in jeder Komponente wiederholt werden
<script setup>import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()</script>Logik einmal schreiben, überall nutzen
Composable erstellen: useMouse
Abschnitt betitelt „Composable erstellen: useMouse“import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() { const x = ref(0) const y = ref(0)
function update(event) { x.value = event.pageX y.value = event.pageY }
onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update))
// Reaktive Werte zurückgeben return { x, y }}Composable verwenden
Abschnitt betitelt „Composable verwenden“<script setup>import { useMouse } from '@/composables/useMouse'
// Destrukturierung - x und y sind Refsconst { x, y } = useMouse()</script>
<template> <div> Mausposition: {{ x }} / {{ y }} </div></template>Der Lifecycle (onMounted, onUnmounted) wird automatisch an die Komponente gebunden, die das Composable aufruft.
Composable mit Parametern
Abschnitt betitelt „Composable mit Parametern“import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) { const data = ref(null) const error = ref(null) const isLoading = ref(false)
watchEffect(async () => { isLoading.value = true error.value = null
try { const response = await fetch(toValue(url)) // toValue für Ref oder Getter data.value = await response.json() } catch (e) { error.value = e } finally { isLoading.value = false } })
return { data, error, isLoading }}<script setup>import { ref, computed } from 'vue'import { useFetch } from '@/composables/useFetch'
const userId = ref(1)
// URL als Getter - reaktiv bei Änderung von userIdconst { data: user, error, isLoading } = useFetch( () => `/api/users/${userId.value}`)
function nextUser() { userId.value++ // Löst automatisch neuen Fetch aus}</script>
<template> <div v-if="isLoading">Laden...</div> <div v-else-if="error">Fehler: {{ error.message }}</div> <div v-else>{{ user?.name }}</div> <button @click="nextUser">Nächster User</button></template>Composable: useLocalStorage
Abschnitt betitelt „Composable: useLocalStorage“import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) { // Initialen Wert aus Storage lesen const stored = localStorage.getItem(key) const value = ref(stored ? JSON.parse(stored) : defaultValue)
// Bei Änderung in Storage schreiben watch(value, (newValue) => { localStorage.setItem(key, JSON.stringify(newValue)) }, { deep: true })
return value}<script setup>import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')const settings = useLocalStorage('settings', { notifications: true })</script>Composable: useCounter
Abschnitt betitelt „Composable: useCounter“import { ref, computed } from 'vue'
export function useCounter(initialValue = 0, options = {}) { const { min = -Infinity, max = Infinity } = options
const count = ref(initialValue)
const isAtMin = computed(() => count.value <= min) const isAtMax = computed(() => count.value >= max)
function increment() { if (count.value < max) count.value++ }
function decrement() { if (count.value > min) count.value-- }
function reset() { count.value = initialValue }
return { count, isAtMin, isAtMax, increment, decrement, reset }}<script setup>import { useCounter } from '@/composables/useCounter'
const { count, increment, decrement, reset, isAtMax } = useCounter(0, { min: 0, max: 10})</script>
<template> <div class="flex gap-4 items-center"> <button @click="decrement">-</button> <span>{{ count }}</span> <button @click="increment" :disabled="isAtMax">+</button> <button @click="reset">Reset</button> </div></template>Composables kombinieren
Abschnitt betitelt „Composables kombinieren“import { ref, computed } from 'vue'import { useFetch } from './useFetch'import { useDebounce } from './useDebounce'
export function useUserSearch() { const searchTerm = ref('')
// Debounce den Suchbegriff (300ms) const debouncedSearch = useDebounce(searchTerm, 300)
// URL nur wenn Suchbegriff lang genug const url = computed(() => debouncedSearch.value.length >= 2 ? `/api/users?search=${debouncedSearch.value}` : null )
// Fetch nur wenn URL vorhanden const { data: users, isLoading } = useFetch(url)
return { searchTerm, users, isLoading }}Composables vs Mixins
Abschnitt betitelt „Composables vs Mixins“- Namenskonflikte möglich
- Unklare Datenherkunft
- Keine Parameter
- Schwer zu typisieren
- Implizite Abhängigkeiten
export default { mixins: [mouseTracking, validation], // Woher kommt this.x? // Überschreibt validation etwas?}- Klare Namensräume
- Explizite Importe
- Flexible Parameter
- Volle TypeScript-Unterstützung
- Explizite Abhängigkeiten
const { x, y } = useMouse()const { isValid } = useValidation(form)// Herkunft immer klar!Composables mit TypeScript
Abschnitt betitelt „Composables mit TypeScript“import { ref, computed, type Ref, type ComputedRef } from 'vue'
interface UseCounterOptions { min?: number max?: number}
interface UseCounterReturn { count: Ref<number> isAtMin: ComputedRef<boolean> isAtMax: ComputedRef<boolean> increment: () => void decrement: () => void reset: () => void}
export function useCounter( initialValue = 0, options: UseCounterOptions = {}): UseCounterReturn { // ... Implementation}Best Practices
Abschnitt betitelt „Best Practices“usePräfix verwenden- Refs zurückgeben (nicht
.value) - Parameter mit
toValue()normalisieren - Cleanup in
onUnmounted - Kleine, fokussierte Composables
- In
/composablesOrdner organisieren
- Composables in Schleifen/Bedingungen
- Globalen State ohne Vorsicht
- Zu viel in einem Composable
- Synchrone Side Effects im Top-Level
- Composables außerhalb von
setup()
VueUse: Composables-Bibliothek
Abschnitt betitelt „VueUse: Composables-Bibliothek“VueUse ist eine Sammlung von 200+ fertigen Composables für häufige Aufgaben.
npm install @vueuse/core<script setup>import { useMouse, useLocalStorage, useDark, useClipboard, useIntersectionObserver, useFetch} from '@vueuse/core'
const { x, y } = useMouse()const isDark = useDark()const { copy, copied } = useClipboard()</script>Dokumentation
Abschnitt betitelt „Dokumentation“Offizielle Vue Dokumentation:
- Composables - Grundlagen
- Composables vs Other Techniques
Ressourcen:
- VueUse - Composables-Bibliothek
- Vue Composition API FAQ