Vue 3 Composition API: Der vollständige Leitfaden zur Reaktivität
Die Vue 3 Composition API praxisnah erklärt: ref, reactive, computed, watch und Composables für leistungsstarke Vue-Anwendungen.

Die Vue 3 Composition API stellt einen grundlegenden Wandel in der Strukturierung von Vue-Komponenten dar. Dieser Ansatz organisiert Code nach Funktionalität statt nach Optionen, was die Wiederverwendung von Logik und die Wartung komplexer Anwendungen deutlich vereinfacht.
Dieser Leitfaden setzt Grundkenntnisse in Vue.js voraus. Alle Beispiele verwenden die mit Vue 3.2 eingeführte <script setup>-Syntax, die mittlerweile als empfohlener Ansatz gilt.
Reaktivität verstehen mit ref und reactive
Reaktivität bildet das Herzstück von Vue 3. Zwei zentrale Primitive erzeugen reaktive Daten: ref für primitive Werte und reactive für komplexe Objekte.
Die Funktion ref erstellt eine reaktive Referenz, die einen Wert umschließt. Um diesen Wert im Script zu lesen oder zu ändern, muss die Eigenschaft .value verwendet werden. Im Template entpackt Vue diese Eigenschaft automatisch.
<script setup>
import { ref } from 'vue'
// Create a ref with initial value 0
const count = ref(0)
// Function that increments the counter
// Note: .value is required in the script
const increment = () => {
count.value++
}
// Reset function
const reset = () => {
count.value = 0
}
</script>
<template>
<!-- In the template, no .value needed -->
<div class="counter">
<p>Counter: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="reset">Reset</button>
</div>
</template>Für Objekte mit mehreren zusammenhängenden Eigenschaften bietet reactive eine natürlichere Syntax, bei der .value nicht benötigt wird.
<script setup>
import { reactive } from 'vue'
// reactive for complex objects
// All properties are automatically reactive
const user = reactive({
name: 'Jane Smith',
email: 'jane@example.com',
preferences: {
theme: 'dark',
notifications: true
}
})
// Direct property modification (no .value)
const updateTheme = (newTheme) => {
user.preferences.theme = newTheme
}
// Warning: do not reassign the entire object
// user = { ... } would break reactivity
</script>
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>Theme: {{ user.preferences.theme }}</p>
</div>
</template>Als Faustregel gilt: ref für primitive Werte (String, Number, Boolean) und reactive für strukturierte Objekte verwenden.
Berechnete Eigenschaften mit computed
Berechnete Eigenschaften leiten Werte aus dem reaktiven Zustand ab. Sie werden gecacht und nur dann neu berechnet, wenn sich ihre Abhängigkeiten ändern, was sie besonders performant macht.
<script setup>
import { ref, computed } from 'vue'
// Product list
const products = ref([
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Mouse', price: 29, inStock: true },
{ id: 3, name: 'Keyboard', price: 79, inStock: false },
{ id: 4, name: 'Monitor', price: 299, inStock: true }
])
// Active filter
const showOnlyInStock = ref(false)
// computed: automatic filtering based on toggle
// Only recalculates if products or showOnlyInStock changes
const filteredProducts = computed(() => {
if (showOnlyInStock.value) {
return products.value.filter(p => p.inStock)
}
return products.value
})
// computed: total calculation with caching
const totalValue = computed(() => {
return filteredProducts.value.reduce((sum, p) => sum + p.price, 0)
})
// computed: number of displayed items
const productCount = computed(() => filteredProducts.value.length)
</script>
<template>
<div>
<label>
<input type="checkbox" v-model="showOnlyInStock" />
Show only in-stock products
</label>
<p>{{ productCount }} products - Total: ${{ totalValue }}</p>
<ul>
<li v-for="product in filteredProducts" :key="product.id">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</div>
</template>Berechnete Eigenschaften können auch beschreibbar sein, mit einem Getter und Setter. Das ist nützlich für bidirektionale Transformationen.
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// computed with getter AND setter
const fullName = computed({
// Read: combine first and last name
get() {
return `${firstName.value} ${lastName.value}`
},
// Write: split the string into first/last name
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts.slice(1).join(' ') || ''
}
})
</script>
<template>
<!-- Modifying fullName updates firstName and lastName -->
<input v-model="fullName" placeholder="Full name" />
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
</template>Watchers mit watch und watchEffect
Watchers führen Seiteneffekte als Reaktion auf Datenänderungen aus. Vue 3 bietet zwei Ansätze: watch für präzise Kontrolle und watchEffect für automatische Nachverfolgung.
watch bietet präzise Kontrolle über Abhängigkeiten und stellt sowohl den alten als auch den neuen Wert bereit. watchEffect ist einfacher, wenn alle verwendeten reaktiven Abhängigkeiten den Effekt auslösen sollen.
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)
// watch: observes a specific source
// Provides oldValue and newValue, allows debouncing
watch(searchQuery, async (newQuery, oldQuery) => {
console.log(`Search changed from "${oldQuery}" to "${newQuery}"`)
// Don't search if less than 3 characters
if (newQuery.length < 3) {
searchResults.value = []
return
}
isLoading.value = true
try {
// Simulated API call
const response = await fetch(`/api/search?q=${newQuery}`)
searchResults.value = await response.json()
} finally {
isLoading.value = false
}
}, {
// Watcher options
debounce: 300, // Wait 300ms after last keystroke
immediate: false // Don't execute immediately
})
// watchEffect: automatic dependency tracking
// Runs immediately and on every change
watchEffect(() => {
// All refs used here are automatically tracked
console.log(`Current state: ${searchResults.value.length} results`)
console.log(`Loading: ${isLoading.value}`)
})
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search..." />
<p v-if="isLoading">Loading...</p>
<ul v-else>
<li v-for="result in searchResults" :key="result.id">
{{ result.title }}
</li>
</ul>
</div>
</template>Um verschachtelte Objekte oder Arrays zu beobachten, ist bei watch die Option deep erforderlich.
<script setup>
import { reactive, watch } from 'vue'
const settings = reactive({
display: {
theme: 'light',
fontSize: 14
},
notifications: {
email: true,
push: false
}
})
// deep: true to observe nested changes
watch(
() => settings.display,
(newDisplay) => {
console.log('Display settings changed:', newDisplay)
// Save to localStorage for example
localStorage.setItem('display', JSON.stringify(newDisplay))
},
{ deep: true }
)
</script>Bereit für deine Vue.js / Nuxt.js-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Wiederverwendbare Composables erstellen
Composables sind Funktionen, die wiederverwendbare reaktive Logik kapseln. Dieser Ansatz ist eine der größten Stärken der Composition API, wenn es darum geht, Code zwischen Komponenten zu teilen.
import { ref, watchEffect, toValue } from 'vue'
// Composable for HTTP requests
// Automatically handles loading, errors, and refetching
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
// Reusable fetch function
async function fetchData() {
isLoading.value = true
error.value = null
try {
// toValue() allows accepting a ref or a value
const response = await fetch(toValue(url))
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
}
// watchEffect for automatic refetch if url is a ref
watchEffect(() => {
fetchData()
})
// Expose state and methods
return {
data,
error,
isLoading,
refetch: fetchData
}
}Dieses Composable lässt sich anschließend deklarativ in jeder beliebigen Komponente einsetzen.
<script setup>
import { useFetch } from '@/composables/useFetch'
// Simple composable usage
const { data: users, isLoading, error, refetch } = useFetch('/api/users')
</script>
<template>
<div>
<button @click="refetch" :disabled="isLoading">
Refresh
</button>
<p v-if="isLoading">Loading users...</p>
<p v-else-if="error" class="error">Error: {{ error }}</p>
<ul v-else-if="users">
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</template>Ein weiteres Beispiel zeigt ein Composable zur Verwaltung des Formularzustands mit Validierung.
import { reactive, computed } from 'vue'
// Form management composable
export function useForm(initialValues, validationRules) {
// Form state
const form = reactive({
values: { ...initialValues },
errors: {},
touched: {}
})
// Validate a specific field
const validateField = (field) => {
const rules = validationRules[field]
if (!rules) return true
for (const rule of rules) {
const result = rule(form.values[field])
if (result !== true) {
form.errors[field] = result
return false
}
}
form.errors[field] = null
return true
}
// Validate entire form
const validate = () => {
let isValid = true
for (const field in validationRules) {
if (!validateField(field)) {
isValid = false
}
}
return isValid
}
// computed: form is valid if no errors
const isValid = computed(() => {
return Object.values(form.errors).every(e => !e)
})
// Mark a field as touched (to display errors)
const touch = (field) => {
form.touched[field] = true
validateField(field)
}
// Reset the form
const reset = () => {
form.values = { ...initialValues }
form.errors = {}
form.touched = {}
}
return {
form,
isValid,
validate,
validateField,
touch,
reset
}
}Lifecycle-Hooks verwalten
Die Composition API stellt Lifecycle-Hooks als Funktionen bereit, die innerhalb von Setup aufgerufen werden. Diese Hooks führen Code zu bestimmten Zeitpunkten im Lebenszyklus der Komponente aus.
Seiteneffekte (Timer, Event-Listener, Subscriptions) sollten immer in onUnmounted bereinigt werden, um Speicherlecks zu vermeiden.
<script setup>
import {
ref,
onMounted,
onUnmounted,
onBeforeUpdate,
onUpdated
} from 'vue'
const windowWidth = ref(window.innerWidth)
const updateCount = ref(0)
// Handler for resize
const handleResize = () => {
windowWidth.value = window.innerWidth
}
// onMounted: component inserted into DOM
// Ideal for initial API calls and event listeners
onMounted(() => {
console.log('Component mounted')
window.addEventListener('resize', handleResize)
// Example: initialize a third-party library
// chart = new Chart(chartRef.value, config)
})
// onUnmounted: component removed from DOM
// CRITICAL: clean up all side effects
onUnmounted(() => {
console.log('Component unmounted')
window.removeEventListener('resize', handleResize)
// Clean up subscriptions, timers, etc.
// chart?.destroy()
})
// onBeforeUpdate: before DOM update
onBeforeUpdate(() => {
console.log('Before update')
})
// onUpdated: after DOM update
onUpdated(() => {
updateCount.value++
console.log(`DOM updated (${updateCount.value} times)`)
})
</script>
<template>
<div>
<p>Window width: {{ windowWidth }}px</p>
<p>DOM updates: {{ updateCount }}</p>
</div>
</template>Template Refs und DOM-Zugriff
Template Refs ermöglichen direkten Zugriff auf DOM-Elemente oder Instanzen von Kindkomponenten. Das bleibt nützlich für Fälle, in denen direkte Manipulation erforderlich ist.
<script setup>
import { ref, onMounted } from 'vue'
// ref for the DOM element
// Same name as the ref attribute in the template
const inputRef = ref(null)
// Function to focus the input
const focusInput = () => {
// Access to the native DOM element
inputRef.value?.focus()
}
// Auto-focus on mount
onMounted(() => {
focusInput()
})
// Expose the method to parent if needed
defineExpose({
focus: focusInput
})
</script>
<template>
<div>
<!-- The ref attribute binds the element to inputRef -->
<input ref="inputRef" type="text" placeholder="Auto-focused" />
<button @click="focusInput">Refocus</button>
</div>
</template>Kommunikation zwischen Eltern- und Kindkomponenten mit Props und Emits
Die Composition API modernisiert die Deklaration von Props und Events mit defineProps und defineEmits und bietet dabei eine verbesserte TypeScript-Integration.
<script setup>
// defineProps: declares expected props
// With default values via withDefaults
const props = withDefaults(defineProps<{
title: string
count?: number
items?: string[]
}>(), {
count: 0,
items: () => []
})
// defineEmits: declares emitted events
// Precise payload typing
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'submit', data: { title: string; items: string[] }): void
}>()
// Function that emits an event
const handleSubmit = () => {
emit('submit', {
title: props.title,
items: props.items
})
}
const increment = () => {
emit('update', props.count + 1)
}
</script>
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="handleSubmit">Submit</button>
</div>
</template><script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const currentCount = ref(0)
const items = ref(['Item 1', 'Item 2'])
// Handler for update event
const onUpdate = (newValue) => {
currentCount.value = newValue
}
// Handler for submit event
const onSubmit = (data) => {
console.log('Data submitted:', data)
}
</script>
<template>
<ChildComponent
title="My Component"
:count="currentCount"
:items="items"
@update="onUpdate"
@submit="onSubmit"
/>
</template>Bereit für deine Vue.js / Nuxt.js-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Provide und Inject für Dependency Injection
Um Daten zwischen weit entfernten Komponenten ohne Prop Drilling zu teilen, bietet Vue 3 provide und inject an.
<script setup>
import { provide, ref, readonly } from 'vue'
// Global theme state
const theme = ref('light')
// Function to toggle theme
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// provide: makes available to descendants
// readonly prevents direct modification by children
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>
<template>
<div :class="theme">
<slot />
</div>
</template><script setup>
import { inject } from 'vue'
// inject: retrieves value provided by an ancestor
// Default value if not found
const theme = inject('theme', 'light')
const toggleTheme = inject('toggleTheme', () => {})
</script>
<template>
<div>
<p>Current theme: {{ theme }}</p>
<button @click="toggleTheme">
Switch to {{ theme === 'light' ? 'dark' : 'light' }}
</button>
</div>
</template>Fazit
Die Vue 3 Composition API bietet einen leistungsstarken und flexiblen Ansatz zur Organisation von Code in Vue-Anwendungen. Die wichtigsten Konzepte im Überblick:
- ref für primitive Werte, reactive für komplexe Objekte
- computed für abgeleitete Werte mit automatischem Caching
- watch und watchEffect für reaktive Seiteneffekte
- Composables zum Extrahieren und Wiederverwenden von Logik zwischen Komponenten
- Funktionale Lifecycle-Hooks (
onMounted,onUnmounted, etc.) - provide/inject für Dependency Injection ohne Prop Drilling
Dieser Ansatz erleichtert die Entwicklung wartbarer und testbarer Anwendungen und bietet gleichzeitig eine hervorragende TypeScript-Integration. Der nächste Schritt besteht darin, fortgeschrittene Patterns wie asynchrone Composables und die Integration mit Pinia für globales State Management zu erkunden.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
