Zum Inhalt springen

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.

ComponentA.vue
<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

composables/useMouse.js
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 }
}
<script setup>
import { useMouse } from '@/composables/useMouse'
// Destrukturierung - x und y sind Refs
const { 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.

composables/useFetch.js
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 userId
const { 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>
composables/useLocalStorage.js
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>
composables/useCounter.js
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/useUserSearch.js
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 }
}
  • 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?
}
composables/useCounter.ts
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
}
  • use Präfix verwenden
  • Refs zurückgeben (nicht .value)
  • Parameter mit toValue() normalisieren
  • Cleanup in onUnmounted
  • Kleine, fokussierte Composables
  • In /composables Ordner 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 ist eine Sammlung von 200+ fertigen Composables für häufige Aufgaben.

Terminal-Fenster
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>

Offizielle Vue Dokumentation:

Ressourcen: