Getters & Actions
Berechnete Werte und Funktionen in Pinia Stores – synchron und asynchron.
Getters: Berechnete Werte im Store
Getters sind computed()-Werte in deinem Store. Sie berechnen abgeleitete Daten aus dem State und werden automatisch gecacht – sie werden nur neu berechnet, wenn sich eine Abhängigkeit ändert.
// stores/products.ts
export const useProductStore = defineStore('products', () => {
const products = ref<Product[]>([])
const selectedCategory = ref<string | null>(null)
// Einfacher Getter
const productCount = computed(() => products.value.length)
// Getter mit Filterlogik
const filteredProducts = computed(() => {
if (!selectedCategory.value) return products.value
return products.value.filter(p => p.category === selectedCategory.value)
})
// Getter mit Parameter (gibt eine Funktion zurück)
const getProductById = computed(() => {
return (id: string) => products.value.find(p => p.id === id)
})
// Getter mit Aggregation
const averagePrice = computed(() => {
if (products.value.length === 0) return 0
const total = products.value.reduce((sum, p) => sum + p.price, 0)
return total / products.value.length
})
return {
products, selectedCategory,
productCount, filteredProducts, getProductById, averagePrice,
}
})Rails-Vergleich: Getters
Getters sind wie berechnete Attribute in ActiveRecord-Models. Vergleiche computed(() => ...) mit einem Model-Method wie def full_name; "#{first_name} #{last_name}"; end – nur dass Pinia-Getters automatisch gecacht und reaktiv aktualisiert werden, ähnlich wie Rails Counter-Caches, aber ohne manuellen Invalidierungsaufwand.
Getters die andere Getters verwenden
Da Getters einfach computed()-Werte sind, können sie auf andere Getters und State-Refs zugreifen – alles ist reaktiv verknüpft.
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
// Getter 1: Anzahl aller Artikel
const itemCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
// Getter 2: Gesamtpreis
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// Getter 3: Nutzt andere Getters!
const cartSummary = computed(() =>
`${itemCount.value} Artikel – ${total.value.toFixed(2)} €`
)
// Getter 4: Nutzt State + anderen Getter
const isEmpty = computed(() => itemCount.value === 0)
return { items, itemCount, total, cartSummary, isEmpty }
})Getter-Ketten sind effizient
Vue's Reaktivitätssystem erkennt automatisch die Abhängigkeiten. Wenn sich items ändert, wird itemCount neu berechnet, was wiederum cartSummary auslöst – aber nur wenn nötig.
Actions: Synchron
Actions sind normale Funktionen, die den State verändern. In der Setup-Syntax gibt es keinen Unterschied zwischen „Mutations" und „Actions" – alles sind einfach Funktionen.
// stores/cart.ts – Actions
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
function addItem(product: Product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(productId: string) {
const index = items.value.findIndex(i => i.id === productId)
if (index > -1) {
if (items.value[index].quantity > 1) {
items.value[index].quantity--
} else {
items.value.splice(index, 1)
}
}
}
function clear() {
items.value = []
}
return { items, addItem, removeItem, clear }
}) Anders als Vuex braucht Pinia keine mutations. Du änderst State direkt in Actions – weniger Boilerplate, gleiche DevTools-Integration.
Actions: Asynchron
Async Actions sind perfekt für API-Calls. Du kannst async/await direkt verwenden – kein spezielles Middleware-System nötig.
// stores/products.ts
export const useProductStore = defineStore('products', () => {
const products = ref<Product[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchProducts() {
loading.value = true
error.value = null
try {
products.value = await $fetch('/api/products')
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unbekannter Fehler'
} finally {
loading.value = false
}
}
async function createProduct(data: CreateProductDTO) {
const newProduct = await $fetch('/api/products', {
method: 'POST',
body: data,
})
// Optimistisch zum State hinzufügen
products.value.push(newProduct)
}
return { products, loading, error, fetchProducts, createProduct }
})Rails-Vergleich: Async Actions
Async Actions sind vergleichbar mit Service Objects in Rails. Wie ein OrderCreator.call(params) orchestrieren sie mehrere Schritte (API-Call, Validierung, State-Änderung) in einer Funktion. Der Unterschied: In Rails ist das serverseitig und synchron pro Request – in Pinia ist es clientseitig und non-blocking.
Actions die andere Actions aufrufen
Actions können andere Actions aufrufen – auch aus anderen Stores. So baust du komplexe Workflows aus kleinen, testbaren Bausteinen.
// stores/checkout.ts
export const useCheckoutStore = defineStore('checkout', () => {
const processing = ref(false)
async function placeOrder() {
const cartStore = useCartStore() // Anderen Store verwenden
const userStore = useUserStore()
if (cartStore.isEmpty) {
throw new Error('Warenkorb ist leer')
}
processing.value = true
try {
await $fetch('/api/orders', {
method: 'POST',
body: {
items: cartStore.items,
userId: userStore.userId,
total: cartStore.total,
},
})
cartStore.clear() // Action eines anderen Stores aufrufen
} finally {
processing.value = false
}
}
return { processing, placeOrder }
})Error Handling in Actions
Speichere Fehler-State direkt im Store. So kann jede Komponente auf Fehler reagieren – ob Fehlermeldung, Retry-Button oder Weiterleitung.
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function login(email: string, password: string) {
loading.value = true
error.value = null
try {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password },
})
user.value = response.user
} catch (e: any) {
// Fehler-State im Store speichern
if (e.statusCode === 401) {
error.value = 'E-Mail oder Passwort falsch'
} else if (e.statusCode === 429) {
error.value = 'Zu viele Versuche – bitte warte kurz'
} else {
error.value = 'Ein unerwarteter Fehler ist aufgetreten'
}
throw e // Optional: Weiterwerfen für catch in der Komponente
} finally {
loading.value = false
}
}
function clearError() {
error.value = null
}
return { user, loading, error, login, clearError }
})<script setup lang="ts">
import { storeToRefs } from 'pinia'
const authStore = useAuthStore()
const { loading, error } = storeToRefs(authStore)
const email = ref('')
const password = ref('')
async function handleSubmit() {
try {
await authStore.login(email.value, password.value)
await navigateTo('/dashboard')
} catch {
// Fehler ist bereits im Store – Anzeige erfolgt reaktiv
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<!-- Fehleranzeige direkt aus dem Store -->
<div v-if="error" class="bg-red-500/10 text-red-400 p-3 rounded-lg mb-4">
{{ error }}
</div>
<input v-model="email" type="email" @focus="authStore.clearError()" />
<input v-model="password" type="password" />
<button :disabled="loading">
{{ loading ? 'Wird geladen...' : 'Anmelden' }}
</button>
</form>
</template>$patch: Mehrere State-Änderungen gebündelt
Mit $patch() kannst du mehrere State-Änderungen in einem Schritt durchführen. Das ist effizienter als einzelne Zuweisungen, da Vue die Reaktivitäts-Updates bündelt.
const cartStore = useCartStore()
// Variante 1: Objekt – einfach und deklarativ
cartStore.$patch({
items: [],
lastCleared: new Date(),
})
// Variante 2: Funktion – für komplexere Logik
cartStore.$patch((state) => {
state.items = state.items.filter(i => i.quantity > 0)
state.lastUpdated = new Date()
})
// Ohne $patch: Zwei separate Reaktivitäts-Zyklen
cartStore.items = [] // → Vue reagiert
cartStore.lastCleared = new Date() // → Vue reagiert nochmal
// Mit $patch: Ein Reaktivitäts-Zyklus
cartStore.$patch({
items: [],
lastCleared: new Date(),
}) // → Vue reagiert nur einmal ✅Wann $patch verwenden?
Verwende $patch() wenn du mehrere State-Werte gleichzeitig ändern willst – besonders bei Objekten und Arrays. Für einzelne Werte reicht eine direkte Zuweisung.
Demo: Warenkorb-Store
Ein vollständiger Warenkorb mit Getters für Berechnungen und Actions für alle Operationen.
// stores/cart.ts – vollständiger Warenkorb-Store
interface CartItem {
id: string
name: string
price: number
quantity: number
}
export const useCartStore = defineStore('cart', () => {
// ── State ──
const items = ref<CartItem[]>([])
// ── Getters ──
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
)
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
)
const isEmpty = computed(() => items.value.length === 0)
const formattedTotal = computed(() =>
new Intl.NumberFormat('de-DE', {
style: 'currency', currency: 'EUR',
}).format(total.value)
)
// ── Actions ──
function addItem(product: { id: string; name: string; price: number }) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(productId: string) {
const idx = items.value.findIndex(i => i.id === productId)
if (idx === -1) return
items.value[idx].quantity > 1
? items.value[idx].quantity--
: items.value.splice(idx, 1)
}
function clear() {
items.value = []
}
return {
items, itemCount, total, isEmpty, formattedTotal,
addItem, removeItem, clear,
}
})Produkte
Warenkorb (0 Artikel)
Zusammenfassung
- ✅ Getters =
computed()– gecacht und reaktiv - ✅ Getters können andere Getters referenzieren
- ✅ Sync Actions – direkte State-Änderungen
- ✅ Async Actions – API-Calls mit
async/await - ✅ Error State im Store für einheitliches Fehler-Handling
- ✅ $patch() für gebündelte State-Änderungen