Vue.js & Frontend

Vue 3 Composition API mit Claude Code 2026

Vue 3 Composition API mit ref, reactive und Composables — Claude Code baut moderne Vue-Apps mit Pinia, TypeScript und Script Setup.

📅 6. Mai 2026 ⏱ 11 min Lesezeit 🏷 Vue.js & Frontend
Vue 3 Composables Pinia TypeScript

Inhalt

  1. Script Setup und Reactivity
  2. Computed und Watch
  3. Composables
  4. Pinia State Management
  5. Lifecycle Hooks und Template Refs
  6. TypeScript-Integration

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

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>
ref() vs. reactive() — Wann was?

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>
APIAuto-TrackingSofortOld/New-WertTypisch für
watchEffect()✅ Ja✅ Ja❌ NeinSide Effects, Fetching
watch()❌ Explizitopt.✅ JaReaktion auf Änderungen
computed()✅ Ja✅ Ja❌ NeinAbgeleitete 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

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 — Die Composables-Bibliothek

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 vs. Vuex

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>
provide/inject mit InjectionKey

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

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:

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