Einfaches State Management
Vue’s Reaktivitätssystem (ref, reactive, computed) funktioniert auch außerhalb von Komponenten. Damit lässt sich einfaches State Management ohne zusätzliche Libraries umsetzen.
Wann reicht einfaches State Management?
Abschnitt betitelt „Wann reicht einfaches State Management?“- Wenige geteilte Zustände (1-3 Stores)
- Kleines Team / Solo-Projekt
- Überschaubare Anwendung
- Keine komplexe Debugging-Anforderungen
- Schneller Prototyp
- Viele geteilte Zustände
- DevTools Debugging wichtig
- Team-Entwicklung
- Komplexe State-Logik
- SSR (Server-Side Rendering)
- Plugins für Persistenz, etc.
Grundprinzip: Reaktiver State außerhalb von Komponenten
Abschnitt betitelt „Grundprinzip: Reaktiver State außerhalb von Komponenten“import { ref, computed } from 'vue'
// State - wird einmal erstellt und geteiltconst count = ref(0)
// Getterconst doubleCount = computed(() => count.value * 2)
// Actionsfunction increment() { count.value++}
function reset() { count.value = 0}
// Alles exportierenexport { count, doubleCount, increment, reset }<!-- Komponente A --><script setup>import { count, increment } from '@/stores/counter'</script>
<template> <button @click="increment">Count: {{ count }}</button></template><!-- Komponente B - sieht denselben State! --><script setup>import { count, doubleCount } from '@/stores/counter'</script>
<template> <p>Count: {{ count }}, Double: {{ doubleCount }}</p></template>Als Composable (Empfohlen)
Abschnitt betitelt „Als Composable (Empfohlen)“Die sauberere Variante kapselt alles in einem Composable:
import { ref, computed, readonly } from 'vue'
// State außerhalb der Funktion = Singletonconst count = ref(0)
export function useCounter() { const doubleCount = computed(() => count.value * 2)
function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = 0 }
return { // readonly verhindert direkte Mutation von außen count: readonly(count), doubleCount, increment, decrement, reset }}<script setup>import { useCounter } from '@/composables/useCounter'
const { count, doubleCount, increment, reset } = useCounter()</script>
<template> <div> <p>Count: {{ count }}</p> <p>Double: {{ doubleCount }}</p> <button @click="increment">+1</button> <button @click="reset">Reset</button> </div></template>Mit reactive() für komplexeren State
Abschnitt betitelt „Mit reactive() für komplexeren State“import { reactive, computed, readonly } from 'vue'
const state = reactive({ user: null, token: null, isLoading: false})
export function useAuth() { const isLoggedIn = computed(() => !!state.token) const userName = computed(() => state.user?.name ?? 'Gast')
async function login(credentials) { state.isLoading = true try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }) const data = await response.json() state.token = data.token state.user = data.user } finally { state.isLoading = false } }
function logout() { state.user = null state.token = null }
return { state: readonly(state), isLoggedIn, userName, login, logout }}Praktisches Beispiel: Todo-Store
Abschnitt betitelt „Praktisches Beispiel: Todo-Store“import { ref, computed, readonly } from 'vue'
const todos = ref([])
export function useTodos() { // Computed Getters const completedCount = computed(() => todos.value.filter(t => t.done).length )
const pendingCount = computed(() => todos.value.filter(t => !t.done).length )
const allDone = computed(() => todos.value.length > 0 && pendingCount.value === 0 )
// Actions function addTodo(text) { todos.value.push({ id: Date.now(), text, done: false }) }
function toggleTodo(id) { const todo = todos.value.find(t => t.id === id) if (todo) { todo.done = !todo.done } }
function removeTodo(id) { const index = todos.value.findIndex(t => t.id === id) if (index !== -1) { todos.value.splice(index, 1) } }
function clearCompleted() { todos.value = todos.value.filter(t => !t.done) }
return { todos: readonly(todos), completedCount, pendingCount, allDone, addTodo, toggleTodo, removeTodo, clearCompleted }}<script setup>import { ref } from 'vue'import { useTodos } from '@/composables/useTodos'
const { todos, pendingCount, addTodo, toggleTodo, removeTodo } = useTodos()const newTodo = ref('')
function handleSubmit() { if (newTodo.value.trim()) { addTodo(newTodo.value) newTodo.value = '' }}</script>
<template> <form @submit.prevent="handleSubmit"> <input v-model="newTodo" placeholder="Neue Aufgabe..." /> <button type="submit">Hinzufügen</button> </form>
<p>{{ pendingCount }} Aufgaben offen</p>
<ul> <li v-for="todo in todos" :key="todo.id"> <input type="checkbox" :checked="todo.done" @change="toggleTodo(todo.id)" /> <span :class="{ done: todo.done }">{{ todo.text }}</span> <button @click="removeTodo(todo.id)">×</button> </li> </ul></template>Mit Provide/Inject kombinieren
Abschnitt betitelt „Mit Provide/Inject kombinieren“Für scoped State (nur in einem Teilbaum der App):
import { ref, computed, provide, inject, readonly } from 'vue'
const CartSymbol = Symbol('cart')
export function provideCart() { const items = ref([])
const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) )
const itemCount = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) )
function addItem(product) { const existing = items.value.find(i => i.id === product.id) if (existing) { existing.quantity++ } else { items.value.push({ ...product, quantity: 1 }) } }
function removeItem(id) { const index = items.value.findIndex(i => i.id === id) if (index !== -1) items.value.splice(index, 1) }
const cart = { items: readonly(items), total, itemCount, addItem, removeItem }
provide(CartSymbol, cart) return cart}
export function useCart() { const cart = inject(CartSymbol) if (!cart) { throw new Error('useCart() requires provideCart() in parent') } return cart}<!-- App.vue oder Layout --><script setup>import { provideCart } from '@/composables/useCartProvider'
provideCart() // Cart für alle Kinder verfügbar</script><!-- Tief verschachtelte Komponente --><script setup>import { useCart } from '@/composables/useCartProvider'
const { items, total, addItem } = useCart()</script>Persistenz mit localStorage
Abschnitt betitelt „Persistenz mit localStorage“import { ref, watch } from 'vue'
function loadState(key, defaultValue) { const saved = localStorage.getItem(key) return saved ? JSON.parse(saved) : defaultValue}
const count = ref(loadState('counter', 0))
// Bei jeder Änderung speichernwatch(count, (newValue) => { localStorage.setItem('counter', JSON.stringify(newValue))}, { deep: true })
export function usePersistedCounter() { function increment() { count.value++ }
function reset() { count.value = 0 }
return { count, increment, reset }}Vergleich: Einfacher State vs Pinia
Abschnitt betitelt „Vergleich: Einfacher State vs Pinia“| Aspekt | Einfacher State | Pinia |
|---|---|---|
| Setup | Keine Dependencies | npm install |
| Bundle-Größe | 0 KB | ~2 KB |
| DevTools | Keine | Vollständig |
| TypeScript | Manual | Automatisch |
| SSR | Problematisch | Eingebaut |
| Plugins | Manual | Ecosystem |
| Hot Reload | Teilweise | Vollständig |
| Learning Curve | Minimal | Gering |
Wann zu Pinia wechseln?
Abschnitt betitelt „Wann zu Pinia wechseln?“- Du brauchst DevTools zum Debuggen
- Mehrere Stores mit Abhängigkeiten
- SSR wird benötigt
- Team wächst
- State-Persistenz-Plugin gewünscht
- Komplexe async Actions
const count = ref(0)export function useCounter() { return { count, increment: () => count.value++ }}
// Nachher: stores/counter.jsimport { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', () => { const count = ref(0) function increment() { count.value++ } return { count, increment }})Die Setup-Syntax von Pinia ist fast identisch!
Best Practices
Abschnitt betitelt „Best Practices“readonly()für exponierten State- Composable-Pattern für Kapselung
- Klare Trennung: State, Getters, Actions
- Beschreibende Namen (
useTodos,useAuth) - State außerhalb der Funktion für Singleton
- State direkt exportieren ohne Wrapper
- Zu viele Stores ohne Organisation
- Komplexe Abhängigkeiten zwischen Stores
- SSR ohne entsprechende Vorkehrungen
Weiterführend
Abschnitt betitelt „Weiterführend“- Pinia - Für komplexere Anforderungen
- Provide/Inject - Scoped State
- Composables - Wiederverwendbare Logik
- Reaktivität - Vue’s Reaktivitätssystem