Zum Inhalt springen

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.

  • Wenige geteilte Zustände (1-3 Stores)
  • Kleines Team / Solo-Projekt
  • Überschaubare Anwendung
  • Keine komplexe Debugging-Anforderungen
  • Schneller Prototyp

Grundprinzip: Reaktiver State außerhalb von Komponenten

Abschnitt betitelt „Grundprinzip: Reaktiver State außerhalb von Komponenten“
stores/counter.js
import { ref, computed } from 'vue'
// State - wird einmal erstellt und geteilt
const count = ref(0)
// Getter
const doubleCount = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
function reset() {
count.value = 0
}
// Alles exportieren
export { 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>

Die sauberere Variante kapselt alles in einem Composable:

composables/useCounter.js
import { ref, computed, readonly } from 'vue'
// State außerhalb der Funktion = Singleton
const 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>
composables/useAuth.js
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
}
}
composables/useTodos.js
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>

Für scoped State (nur in einem Teilbaum der App):

composables/useCartProvider.js
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>
composables/usePersistedCounter.js
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 speichern
watch(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 }
}
AspektEinfacher StatePinia
SetupKeine Dependenciesnpm install
Bundle-Größe0 KB~2 KB
DevToolsKeineVollständig
TypeScriptManualAutomatisch
SSRProblematischEingebaut
PluginsManualEcosystem
Hot ReloadTeilweiseVollständig
Learning CurveMinimalGering
  • 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
  • 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