Inhalt
Die Vue 3 Composition API hat die Art, wie wir Vue-Anwendungen schreiben, grundlegend verändert. Mit Script Setup, Composables und Pinia entsteht Code, der wartbarer, testbarer und wiederverwendbarer ist als je zuvor. Claude Code unterstützt Entwickler dabei, diese Patterns schnell und korrekt umzusetzen — von einfachen Reaktivitäts-Primitiven bis hin zu komplexen State-Management-Lösungen. Dieser Guide zeigt alle wesentlichen Konzepte mit praxisnahen Beispielen.
1. Script Setup und Reactivity
<script setup> ist der empfohlene Einstiegspunkt für die Composition API in Vue 3.
Alles, was auf der obersten Ebene deklariert wird — Variablen, Funktionen, Importe — steht dem
Template automatisch zur Verfügung. Kein explizites return {} mehr.
🟢 Options API vs. Composition API
- Options API: data(), methods, computed, watch als separate Objekte — Code-Logik verteilt
- Composition API: Logik nach Feature gruppiert, nicht nach Option
- Script Setup: Syntactic Sugar — weniger Boilerplate, bessere TypeScript-Unterstützung
- Migrationspfad: Beide APIs koexistieren — schrittweise Migration möglich
ref() — Primitives reaktiv machen
ref() erstellt ein reaktives Objekt mit einer .value-Eigenschaft.
Im Template wird .value automatisch ausgepackt.
<script setup>
import { ref, readonly, toRefs } from 'vue'
// ref() für primitive Werte (String, Number, Boolean)
const count = ref(0)
const title = ref('')
const isLoading = ref(false)
// Wert lesen und setzen — immer .value
function increment() {
count.value++
}
// readonly() — schreibgeschützter Ref
const readonlyCount = readonly(count)
// ref() für Objekte (intern reactive)
const user = ref({ name: 'Anna', age: 28 })
user.value.name = 'Max' // direktes Mutieren funktioniert
</script>
<template>
<!-- .value wird im Template automatisch ausgepackt -->
<button @click="increment">Count: {{ count }}</button>
<p>{{ user.name }} ist {{ user.age }} Jahre alt</p>
</template>
reactive() — Objekte vollständig reaktiv
reactive() gibt ein vollständig reaktives Proxy-Objekt zurück.
Im Gegensatz zu ref() kein .value notwendig — dafür kein Reassign möglich.
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
name: 'Vue 3',
items: [] as string[]
})
// Direkter Zugriff — kein .value nötig
function addItem(item: string) {
state.items.push(item)
state.count++
}
// toRefs() — destructuring ohne Reaktivitätsverlust
const { count, name, items } = toRefs(state)
// Jetzt sind count, name, items Refs mit .value
// VORSICHT: Direkte Destructuring zerstört Reaktivität!
// const { count } = state // ❌ NICHT reaktiv!
</script>
Als Faustregel gilt: ref() für einzelne Werte und primitive Typen, reactive() für zusammengehörige Objekte mit mehreren Properties. In der Praxis ist ref() flexibler, da es auch Objekte aufnehmen kann und beim Weitergeben (z.B. an Composables) die Reaktivität erhält.
2. Computed und Watch
Computed Properties und Watcher sind das Herzstück reaktiver Datenflüsse in Vue.
Die Composition API bietet mit computed(), watch() und
watchEffect() drei mächtige Werkzeuge für unterschiedliche Anwendungsfälle.
computed() — Abgeleitete Werte
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Max')
const lastName = ref('Mustermann')
const items = ref(['Apple', 'Banana', 'Cherry'])
const search = ref('')
// Einfache computed property — automatisch gecacht
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// Computed mit getter und setter (writable computed)
const writableFullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
[firstName.value, lastName.value] = val.split(' ')
}
})
// Computed für Filterung — nur neu berechnet wenn items oder search sich ändern
const filteredItems = computed(() =>
items.value.filter(item =>
item.toLowerCase().includes(search.value.toLowerCase())
)
)
</script>
watchEffect() — Automatisches Dependency-Tracking
watchEffect() läuft sofort und trackt alle reaktiven Abhängigkeiten
automatisch — kein explizites Definieren der Sources notwendig.
<script setup>
import { ref, watchEffect, onUnmounted } from 'vue'
const userId = ref(1)
const userData = ref(null)
// Läuft sofort und bei jeder Änderung von userId
const stop = watchEffect(async () => {
userData.value = await fetchUser(userId.value)
})
// Cleanup-Callback für Side-Effects
watchEffect((onCleanup) => {
const timer = setInterval(() => console.log('tick'), 1000)
onCleanup(() => clearInterval(timer))
})
// Watcher manuell stoppen
stop()
</script>
watch() — Explizite Quellen mit voller Kontrolle
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const state = reactive({ items: [1, 2, 3] })
// Einzelne Source beobachten
watch(count, (newVal, oldVal) => {
console.log(`Count: ${oldVal} → ${newVal}`)
})
// Mehrere Sources gleichzeitig
const a = ref('x'), b = ref('y')
watch([a, b], ([newA, newB], [oldA, oldB]) => {
console.log('a oder b hat sich geändert')
})
// immediate: sofort beim Start auslösen
watch(count, (val) => syncToServer(val), {
immediate: true
})
// deep: verschachtelte Objekte beobachten
watch(() => state.items, (items) => {
console.log('Items geändert:', items)
}, { deep: true })
</script>
| API | Auto-Tracking | Sofort | Old/New-Wert | Typisch für |
|---|---|---|---|---|
watchEffect() | ✅ Ja | ✅ Ja | ❌ Nein | Side Effects, Fetching |
watch() | ❌ Explizit | opt. | ✅ Ja | Reaktion auf Änderungen |
computed() | ✅ Ja | ✅ Ja | ❌ Nein | Abgeleitete Werte |
3. Composables
Composables sind Funktionen, die Composition API-Logik kapseln und wiederverwenden. Sie ersetzen Mixins und bieten dabei volle TypeScript-Unterstützung, klare Abhängigkeiten und einfaches Testing. Claude Code kann Composables aus vorhandenem Code extrahieren.
🔷 Composable-Konventionen
- Dateiname beginnt mit
use(z.B.useCounter.ts) - Gibt reaktive Werte und Funktionen zurück
- Kann Lifecycle Hooks intern registrieren
- Abgelegt in
src/composables/
useCounter — Einfaches Beispiel
// src/composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment(step = 1) { count.value += step }
function decrement(step = 1) { count.value -= step }
function reset() { count.value = initialValue }
return { count, doubled, increment, decrement, reset }
}
// Verwendung in einer Komponente:
const { count, doubled, increment, reset } = useCounter(10)
useFetch — Async Composable mit Cleanup
// src/composables/useFetch.ts
import { ref, watch, type Ref } from 'vue'
interface FetchState<T> {
data: Ref<T | null>
error: Ref<string | null>
isLoading: Ref<boolean>
refetch: () => void
}
export function useFetch<T>(url: string | Ref<string>): FetchState<T> {
const data = ref<T | null>(null)
const error = ref<string | null>(null)
const isLoading = ref(false)
let controller: AbortController
async function fetchData() {
if (controller) controller.abort()
controller = new AbortController()
isLoading.value = true
error.value = null
try {
const endpoint = typeof url === 'string' ? url : url.value
const res = await fetch(endpoint, { signal: controller.signal })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
if ((e as Error).name !== 'AbortError')
error.value = (e as Error).message
} finally {
isLoading.value = false
}
}
if (typeof url !== 'string') watch(url, fetchData, { immediate: true })
else fetchData()
return { data, error, isLoading, refetch: fetchData }
}
useLocalStorage — Persistenz mit Reaktivität
// src/composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const stored = localStorage.getItem(key)
const value = ref<T>(stored ? JSON.parse(stored) : defaultValue)
watch(value, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
}, { deep: true })
return value
}
// In der Komponente:
const theme = useLocalStorage('theme', 'dark')
const preferences = useLocalStorage('prefs', { lang: 'de', notifications: true })
VueUse bietet über 200 fertige Composables: useScroll, useDark, useClipboard, useWebSocket, useIntersectionObserver und viele mehr. Claude Code kennt die VueUse-API und kann Composables direkt einsetzen oder als Vorlage nutzen.
4. Pinia State Management
Pinia ist der offizielle State Manager für Vue 3. Leichter als Vuex, vollständig typesicher und mit nativer Devtools-Integration. Claude Code generiert Pinia Stores direkt aus Anforderungen — inklusive Actions, Getters und Plugins.
defineStore() — Store erstellen
// src/stores/userStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
// Setup Store — wie eine Composition API-Funktion
export const useUserStore = defineStore('user', () => {
// State (refs)
const currentUser = ref<User | null>(null)
const isAuthenticated = ref(false)
const isLoading = ref(false)
// Getters (computed)
const isAdmin = computed(() => currentUser.value?.role === 'admin')
const displayName = computed(() => currentUser.value?.name ?? 'Gast')
// Actions (functions)
async function login(email: string, password: string) {
isLoading.value = true
try {
const user = await authService.login(email, password)
currentUser.value = user
isAuthenticated.value = true
} finally {
isLoading.value = false
}
}
function logout() {
currentUser.value = null
isAuthenticated.value = false
}
return { currentUser, isAuthenticated, isLoading, isAdmin, displayName, login, logout }
})
storeToRefs() — Reaktivität beim Destructuring
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
// storeToRefs() — State und Getter als reaktive Refs
const { currentUser, isAdmin, displayName, isLoading } = storeToRefs(userStore)
// Actions direkt destrukturieren (keine Refs nötig)
const { login, logout } = userStore
// $patch — mehrere State-Updates auf einmal
userStore.$patch({
isAuthenticated: true,
currentUser: { id: 1, name: 'Anna', email: 'anna@example.com', role: 'user' }
})
// $subscribe — auf State-Änderungen reagieren
userStore.$subscribe((mutation, state) => {
localStorage.setItem('user-backup', JSON.stringify(state.currentUser))
})
</script>
Pinia Plugins — Store erweitern
// src/plugins/persistPlugin.ts
import { type PiniaPluginContext } from 'pinia'
export function persistPlugin({ store }: PiniaPluginContext) {
const key = `pinia-${store.$id}`
const saved = localStorage.getItem(key)
if (saved) store.$patch(JSON.parse(saved))
store.$subscribe((_, state) => {
localStorage.setItem(key, JSON.stringify(state))
})
}
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(persistPlugin)
createApp(App).use(pinia).mount('#app')
Pinia hat Vuex offiziell abgelöst. Neue Vue 3-Projekte sollten immer Pinia nutzen. Kein Mutations-Boilerplate, volle TypeScript-Unterstützung ohne Extra-Typen, modulares Design out-of-the-box und deutlich bessere Devtools-Integration.
5. Lifecycle Hooks und Template Refs
In der Composition API werden Lifecycle Hooks als Funktionen registriert. Sie können innerhalb von Composables verwendet werden — eine der mächtigsten Eigenschaften gegenüber Mixins, da Cleanup-Logik direkt beim Feature-Code liegt.
Lifecycle Hooks im Überblick
<script setup>
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onErrorCaptured
} from 'vue'
onBeforeMount(() => {
console.log('Vor dem ersten Render — DOM noch nicht verfügbar')
})
onMounted(() => {
console.log('DOM bereit — DOM-Zugriff und externe Libs initialisieren')
})
onBeforeUpdate(() => {
console.log('State geändert — vor dem Re-Render')
})
onUpdated(() => {
console.log('DOM nach Re-Render aktualisiert')
})
onBeforeUnmount(() => {
console.log('Komponente wird entfernt — Cleanup starten')
})
onUnmounted(() => {
console.log('Komponente vollständig entfernt')
})
onErrorCaptured((error, instance, info) => {
console.error('Fehler in Kind-Komponente:', error)
return false // Fehler-Propagation stoppen
})
</script>
useTemplateRef() — Template Refs in Vue 3.5+
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// Vue 3.5+: useTemplateRef() statt ref() für DOM-Refs
const inputEl = useTemplateRef<HTMLInputElement>('searchInput')
const chartEl = useTemplateRef<HTMLDivElement>('chart')
onMounted(() => {
// Erst in onMounted verfügbar
inputEl.value?.focus()
// Chart-Library initialisieren
if (chartEl.value) {
initChart(chartEl.value)
}
})
</script>
<template>
<!-- ref-Attribut muss mit useTemplateRef-Key übereinstimmen -->
<input ref="searchInput" type="text" />
<div ref="chart" class="chart-container"></div>
</template>
provide() und inject() — Dependency Injection
// Eltern-Komponente — Provider
<script setup>
import { provide, ref, readonly } from 'vue'
const theme = ref('dark')
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
// readonly verhindert Mutation durch Kind-Komponenten
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>
// Kind-Komponente — Consumer
<script setup>
import { inject, type Ref } from 'vue'
// Mit Default-Wert
const theme = inject<Ref<string>>('theme', ref('light'))
const toggleTheme = inject<() => void>('toggleTheme', () => {})
</script>
Für typsichere Dependency Injection empfiehlt sich InjectionKey: einen Symbol-basierten Key definieren, der den injizierten Typ trägt. So sind Provider und Consumer typsicher verbunden — auch ohne direkte Importe.
6. TypeScript-Integration
Vue 3 wurde von Grund auf für TypeScript entwickelt. Mit Script Setup und den
Compiler Macros defineProps, defineEmits und defineExpose
ist TypeScript in Vue-Komponenten erstklassig — ohne Runtime-Overhead.
defineProps<T>() — Typsichere Props
<script setup lang="ts">
import { computed, withDefaults } from 'vue'
// Interface-basierte Props-Definition
interface Props {
title: string
count?: number
variant?: 'primary' | 'secondary' | 'danger'
items: string[]
onSave?: (data: { id: number; value: string }) => void
}
// withDefaults() für Default-Werte bei Generic-Props
const props = withDefaults(defineProps<Props>(), {
count: 0,
variant: 'primary',
items: () => []
})
// Props sind reaktiv — computed darauf bauen
const buttonClass = computed(() => `btn btn-${props.variant}`)
</script>
defineEmits<T>() — Typsichere Events
<script setup lang="ts">
import { ref } from 'vue'
// Call Signature-Stil — präziser, besseres Intellisense
const emit = defineEmits<{
change: [value: string]
submit: [payload: { name: string; email: string }]
close: []
'update:modelValue': [value: number]
}>()
const inputValue = ref('')
function handleSubmit() {
emit('submit', { name: 'Max', email: 'max@example.com' })
emit('close')
}
// v-model Support mit update:modelValue
function updateValue(val: number) {
emit('update:modelValue', val)
}
</script>
TypeScript mit ref() und computed()
<script setup lang="ts">
import { ref, computed, reactive } from 'vue'
interface ApiResponse<T> {
data: T
status: number
message: string
}
interface Product {
id: number
name: string
price: number
inStock: boolean
}
// Explizite Typen wo nötig
const products = ref<Product[]>([])
const selectedId = ref<number | null>(null)
const response = ref<ApiResponse<Product[]> | null>(null)
// Computed — Typ wird inferiert
const availableProducts = computed(() =>
products.value.filter(p => p.inStock) // Product[]
)
const selectedProduct = computed(() =>
products.value.find(p => p.id === selectedId.value) ?? null
) // Product | null
// useAttrs — Non-Prop Attribute mit Types
import { useAttrs } from 'vue'
const attrs = useAttrs() // Record<string, unknown>
// defineExpose — öffentliche Komponenten-API
const reset = () => {
products.value = []
selectedId.value = null
}
defineExpose({ reset, products })
</script>
🔷 TypeScript Best Practices mit Vue 3
- lang="ts" in allen Komponenten — nie vermischen
- Interfaces statt Type Aliases für Props und Emits — bessere Fehlermeldungen
- Generic Components mit
generic="T"Attribut in Vue 3.3+ - Strict Mode in tsconfig.json —
"strict": trueist Pflicht - Keine any —
unknownnutzen und Type Guards schreiben - Volar (Vue Official) VSCode Extension für vollständige IDE-Unterstützung
Claude Code als Vue 3 Entwicklungspartner
Claude Code versteht Vue 3 Composition API tief und kann vollständige Komponenten, Composables und Stores generieren. Typische Workflows:
- Komponenten-Scaffolding: "Erstelle eine DataTable-Komponente mit Sortierung und Filterung"
- Composable-Extraktion: Bestehenden Code in wiederverwendbare Composables refaktorieren
- Pinia Migration: Vuex-Stores zu Pinia-Setup-Stores konvertieren
- TypeScript-Upgrade: JavaScript-Komponenten typsicher machen
- Performance: Unnötige Re-Renders durch shallowRef/shallowReactive identifizieren
Vue 3 Apps schneller entwickeln
Nutze Claude Code für deine Vue 3 Projekte — Composition API, Pinia, TypeScript und Composables direkt in deinem Editor. Starte jetzt kostenlos.
Kostenlos testen — kein Kreditkarte nötig