von der ersten Store-Definition bis zur skalierbaren Architektur
Pinia ist nicht einfach nur Vuex 5 mit anderem Namen — es ist ein grundlegend anderer Ansatz, der die Composition API konsequent nutzt. Wer Pinia nur als Datenspeicher nutzt, verschenkt das Potenzial von Store-Komposition, typsicheren Actions und reaktiven Getters. Dieser Artikel zeigt, wie State Management mit Pinia in Vue 3 von Grund auf richtig aufgebaut wird.
Inhaltsverzeichnis
- 1. Warum Pinia und nicht Vuex
- 2. Installation und Einbindung in Vue 3
- 3. Store-Definition: Options vs. Setup-Style
- 4. State: reaktive Daten sauber modellieren
- 5. Getters: abgeleitete Daten ohne Redundanz
- 6. Actions: async-Logik und Fehlerbehandlung
- 7. Store-Komposition: Stores sauber verbinden
- 8. Persistenz und Plugin-System
- 9. Pinia-Muster im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Pinia und nicht Vuex
Die Vue-Community hat sich aus guten Gründen auf Pinia als offiziellen State-Management-Standard geeinigt. Vuex 4 war eine direkte Portierung von Vuex 3 auf Vue 3, die die Composition API nicht konsequent nutzte. Mutations als separate Schicht zwischen State und Actions waren historisch bedingt — in Vue Devtools war es so möglich, jeden State-Übergang zu tracken. Mit Vue 3 und der Proxy-basierten Reaktivität ist diese Trennung nicht mehr notwendig: Pinia trackt State-Änderungen direkt, ohne Mutations als Umweg. Das Ergebnis ist weniger Boilerplate und mehr Typsicherheit.
Der zweite entscheidende Vorteil von Pinia gegenüber Vuex ist das modulare Store-System ohne das Namespacing-Chaos. In Vuex mussten Module mit namespaced: true konfiguriert werden, und der Zugriff auf andere Module erforderte den Store-Root als Kontext. In Pinia ist jeder Store eine eigene Einheit, die direkt importiert und in anderen Stores oder Komponenten verwendet wird. Das macht Store-Komposition natürlich und intuitiv, ohne Root-Store-Referenzen oder String-basierte Dispatch-Aufrufe.
TypeScript-Support war bei Vuex immer eine nachträgliche Zugabe, die nie vollständig integriert war. Pinia wurde von Anfang an mit TypeScript im Kopf designed: State, Getters und Actions werden automatisch korrekt typisiert, ohne manuelle Typdeklarationen für den Store. Das reduziert den Wartungsaufwand erheblich und macht Refactoring sicher, weil der TypeScript-Compiler Verwendungsstellen automatisch findet.
2. Installation und Einbindung in Vue 3
Pinia wird als separates Package installiert und als Vue-Plugin eingebunden. Die Integration in main.ts ist minimal: createPinia() erzeugt die Pinia-Instanz, app.use(pinia) registriert sie. Danach stehen alle Stores sofort in allen Komponenten zur Verfügung — kein manuelles Registrieren einzelner Stores, kein Root-Store-Objekt mit verschachtelten Modulen. Jeder Store-File wird auf Anfrage importiert und automatisch initialisiert.
In Projekten mit serverseitigem Rendering, zum Beispiel mit Nuxt 3, erstellt man für jeden Request eine neue Pinia-Instanz, um State-Vermischung zwischen Requests zu verhindern. Nuxt 3 macht das über das @pinia/nuxt-Modul automatisch. In reinen SPA-Projekten reicht eine globale Instanz. Die Vue Devtools integrieren sich nach der Installation automatisch mit Pinia und zeigen alle Stores mit ihrem aktuellen State, der Änderungshistorie und den ausgeführten Actions in der Timeline-Ansicht an.
// main.ts — Pinia setup in Vue 3
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
// Pinia plugins must be added before app.use(pinia)
// Example: pinia-plugin-persistedstate for localStorage sync
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.mount('#app')
3. Store-Definition: Options vs. Setup-Style
Pinia unterstützt zwei Syntaxvarianten für die Store-Definition. Der Options-Style orientiert sich an der Vuex-API und der Options API von Vue: Ein Objekt mit state, getters und actions als Keys. Der Setup-Style nutzt die Composition API direkt: In einer Funktion werden ref()- und computed()-Werte sowie reguläre Funktionen definiert und am Ende als Objekt zurückgegeben. Beide Varianten erzeugen identische Stores mit identischem Verhalten — der Unterschied ist rein stilistisch.
In TypeScript-Projekten empfiehlt sich der Setup-Style, weil die Typen direkt aus den ref()- und computed()-Deklarationen abgeleitet werden. Kein separates Interface für den Store-State notwendig. Der Options-Style ist für Teams sinnvoll, die von Vuex migrieren und eine vertraute Struktur bevorzugen. Eine Mischung beider Stile im gleichen Projekt ist problemlos möglich und erzeugt keine technischen Schulden. Wichtig: Der erste Parameter von defineStore ist die Store-ID — sie muss projektweqit eindeutig sein und erscheint in den DevTools.
4. State: reaktive Daten sauber modellieren
Der State eines Pinia-Stores ist der einzige Platz für reaktive Datenhaltung. Das klingt trivial, ist aber die häufigste Fehlerquelle in wachsenden Projekten: State wird dupliziert — einmal im Store, einmal als lokales ref() in einer Komponente — und die Synchronisation wird zur manuellen Aufgabe. Die Regel ist klar: Alles, was von mehreren Komponenten gelesen oder verändert wird, gehört in den Pinia-Store. Alles, was ausschließlich lokal für eine Komponente relevant ist, bleibt als ref() oder reactive() in der Komponente.
State-Objekte sollten so flach wie möglich gehalten werden. Tiefe Verschachtelung macht Partial-Updates umständlich und erschwert das Debugging in den DevTools, weil die Änderungshistorie schwerer zu lesen ist. Für komplexe Datenstrukturen empfiehlt sich ein normalisierter State mit ID-basierten Maps — ähnlich wie in Redux mit dem Entities-Pattern. Pinia unterstützt $patch() für atomare Partial-Updates, die als einzelner State-Übergang in den DevTools erscheinen, statt als eine Reihe von Einzelzuweisungen.
5. Getters: abgeleitete Daten ohne Redundanz
Getters in Pinia entsprechen computed()-Properties: Sie werden nur neu berechnet, wenn sich die abhängigen State-Werte ändern, und ihre Ergebnisse werden gecacht. Das macht sie zum richtigen Werkzeug für alle abgeleiteten Daten — gefilterte Listen, aggregierte Zahlen, formatierte Werte. Der häufige Fehler: Getter-Logik in Komponenten zu wiederholen statt einen zentralen Getter zu definieren. Das führt zu Inkonsistenzen, wenn die Berechnungslogik sich ändert.
Getter können andere Getter desselben Stores sowie den State lesen. Im Setup-Style sind das einfach computed()-Aufrufe, die auf andere computed()- oder ref()-Werte zugreifen. Im Options-Style erhält jeder Getter den State als Parameter. Getter können auch parametrisiert werden, indem sie eine Funktion zurückgeben — dann entfällt das automatische Caching, weil das Ergebnis vom Parameter abhängt. Für parametrisierte Abfragen ist es oft sinnvoller, eine Action zu definieren, die das Ergebnis in einer Map im State cached.
// stores/cart.ts — Setup-style Pinia store with getters and actions
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { CartItem, Product } from '@/types'
export const useCartStore = defineStore('cart', () => {
// --- State ---
const items = ref<CartItem[]>([])
const couponCode = ref<string | null>(null)
const isLoading = ref(false)
// --- Getters (computed = cached, reactive) ---
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const discountedTotal = computed(() =>
couponCode.value === 'SAVE10' ? subtotal.value * 0.9 : subtotal.value
)
// --- Actions ---
async function addItem(product: Product, quantity = 1) {
const existing = items.value.find(i => i.productId === product.id)
if (existing) {
existing.quantity += quantity
} else {
items.value.push({ productId: product.id, price: product.price, quantity, name: product.name })
}
}
function removeItem(productId: string) {
items.value = items.value.filter(i => i.productId !== productId)
}
// Atomic patch: single history entry in DevTools
function clearCart() {
items.value = []
couponCode.value = null
}
return { items, couponCode, isLoading, totalItems, subtotal, discountedTotal, addItem, removeItem, clearCart }
})
6. Actions: async-Logik und Fehlerbehandlung
Actions sind der Ort für alle Geschäftslogik im Pinia-Store. Sie können synchron oder asynchron sein und haben direkten Zugriff auf State und Getters. Im Gegensatz zu Vuex gibt es keine Mutations: State-Änderungen in Actions sind direkte Zuweisungen auf ref()-Werte. Pinia trackt diese Änderungen über den Vue-Reaktivitätsproxy und zeigt sie korrekt in den DevTools an. Das bedeutet: Weniger Code, keine Mutation-Namen als Magic Strings und kein zweistufiger Dispatch-Commit-Prozess mehr.
Fehlerbehandlung in asynchronen Actions ist ein häufig vernachlässigter Aspekt. Das Muster: Ein isLoading-Flag und ein error-Ref im State, die in jedem async-Action-Block gesetzt und bei Abschluss zurückgesetzt werden. Mit try/catch/finally ist sichergestellt, dass isLoading auch bei Fehlern korrekt zurückgesetzt wird. Komponenten subscriben sich auf den error-State des Stores und zeigen Fehlermeldungen an, ohne eigene Error-States führen zu müssen.
7. Store-Komposition: Stores sauber verbinden
Store-Komposition ist eines der mächtigsten Features von Pinia und der Hauptunterschied zur Vuex-Modul-Architektur. Ein Pinia-Store kann andere Stores direkt importieren und deren State und Actions nutzen. Ein useOrderStore kann den useCartStore importieren und dessen items lesen, ohne einen Root-Store als Brücke zu benötigen. Das ermöglicht eine natürliche Abhängigkeitsstruktur zwischen Stores, die dem Datenfluss der Anwendung entspricht.
Wichtig bei der Store-Komposition: Zirkuläre Abhängigkeiten vermeiden. Wenn Store A Store B importiert und Store B Store A importiert, entsteht ein Modulauflösungsproblem. Das Muster für solche Fälle ist ein dritter Store, der die gemeinsamen Daten hält, oder das Extrahieren der gemeinsamen Logik in eine Composable-Funktion, die beide Stores nutzen. Pinia zwingt nicht zu einer flachen Store-Hierarchie — die Architektur sollte dem Domänenmodell der Anwendung folgen.
// stores/order.ts — Store composition: reads from cartStore
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useCartStore } from './cart'
import { useAuthStore } from './auth'
import { apiClient } from '@/api'
import type { Order } from '@/types'
export const useOrderStore = defineStore('order', () => {
const orders = ref<Order[]>([])
const isSubmitting = ref(false)
const lastError = ref<string | null>(null)
async function submitOrder(shippingAddress: string) {
// Compose from other stores — no root-store needed
const cart = useCartStore()
const auth = useAuthStore()
if (!auth.isLoggedIn) throw new Error('User not authenticated')
if (cart.items.length === 0) throw new Error('Cart is empty')
isSubmitting.value = true
lastError.value = null
try {
const order = await apiClient.post<Order>('/orders', {
items: cart.items,
total: cart.discountedTotal,
userId: auth.userId,
shippingAddress,
})
orders.value.unshift(order)
// Cross-store side effect: clear cart after successful order
cart.clearCart()
return order
} catch (err) {
lastError.value = err instanceof Error ? err.message : 'Unknown error'
throw err
} finally {
isSubmitting.value = false
}
}
return { orders, isSubmitting, lastError, submitOrder }
})
8. Persistenz und Plugin-System
Pinia bietet ein Plugin-System, das es erlaubt, Stores global mit zusätzlichen Eigenschaften oder Verhalten auszustatten. Das bekannteste Plugin ist pinia-plugin-persistedstate, das ausgewählte State-Teile automatisch im localStorage oder sessionStorage speichert und beim Laden der App wiederherstellt. Die Konfiguration erfolgt direkt in der Store-Definition über eine persist-Option — ohne manuelles Serialize/Deserialize in jeder Action, die State verändert.
Für komplexere Anforderungen — Server-Side-State-Rehydration, Verschlüsselung sensibler Daten, Logging aller State-Übergänge — ist das Plugin-System der richtige Ansatz. Ein Plugin ist eine Funktion, die den Store-Kontext erhält und Store-Properties oder $onAction-Hooks registrieren kann. $onAction ist das Pinia-Äquivalent zu Redux-Middleware: Jede Action wird vor und nach der Ausführung abgefangen, was für Logging, Analytics und Error-Reporting genutzt werden kann. Das hält Tracking-Logik aus den Stores heraus und macht sie in einem zentralen Plugin wartbar.
9. Pinia-Muster im Vergleich
Die Wahl der richtigen Pinia-Architektur hängt von der Projektgröße und den Anforderungen ab. Hier sind die wichtigsten Muster und wann sie eingesetzt werden sollten.
| Szenario | Schlechte Lösung | Empfohlenes Pinia-Muster | Vorteil |
|---|---|---|---|
| Geteilte Daten | Props-Drilling durch 4 Ebenen | Pinia-Store mit ref() | Jede Komponente greift direkt zu |
| Abgeleitete Werte | Berechnung in jeder Komponente | Getter mit computed() | Gecacht, einmalig definiert |
| API-Aufruf | fetch() direkt in Komponente | Async Action im Store | State, Loading, Error zentral |
| Store-Kommunikation | EventBus oder globale Variable | Store-Komposition | Typsicher, nachvollziehbar |
| Persistenz | localStorage in jeder Action | pinia-plugin-persistedstate | Automatisch, konfigurierbar |
Ein häufiger ArchitekturFehler ist das Anlegen zu vieler granularer Stores. Ein Store pro Entität wirkt zunächst ordentlich, führt aber dazu, dass Store-Komposition notwendig wird, wo eine einzige zusammengehörige Store-Einheit ausreichen würde. Die Faustregel: Stores nach Domänen-Bounded-Contexts strukturieren — useCartStore, useAuthStore, useProductStore — nicht nach UI-Ansichten oder Komponenten. State, der nur einer einzigen Ansicht gehört, sollte im lokalen Komponenten-State bleiben.
Mironsoft
Vue 3 Frontend-Architektur und State-Management mit Pinia
Pinia-Architektur für euer Vue 3-Projekt aufbauen?
Wir helfen beim Aufbau einer sauberen Pinia-Storearchitektur — von der ersten Store-Definition bis zu Store-Komposition, Plugin-Integration und TypeScript-Typsicherheit.
Store-Architektur
Domänen-orientierte Stores, saubere Komposition ohne Zirkelabhängigkeiten
TypeScript-Integration
Vollständige Typsicherheit in State, Getters und Actions ohne manuelle Deklarationen
Vuex-Migration
Bestehende Vuex-Stores schrittweise zu Pinia migrieren ohne Feature-Freezes
10. Zusammenfassung
Pinia ist die richtige Wahl für State Management in Vue 3, weil es die Composition API konsequent nutzt statt um sie herumzubauen. Die wichtigsten Prinzipien: Stores nach Domänen strukturieren, nicht nach UI-Ansichten. State flach halten und $patch() für atomare Updates nutzen. Getters für alle abgeleiteten Daten verwenden — nie Berechnungslogik in Komponenten duplizieren. Async Actions mit isLoading- und error-State ausstatten. Store-Komposition statt EventBus für Store-Kommunikation nutzen.
Das Plugin-System macht Pinia erweiterbar für Cross-Cutting-Concerns wie Persistenz, Logging und Analytics. pinia-plugin-persistedstate löst die localStorage-Integration ohne Boilerplate in jeder Action. $onAction-Hooks in eigenen Plugins sind der saubere Ort für Tracking und Error-Reporting. TypeScript-Support ist keine Nachausstattung, sondern designbedingter Teil der API — wer TypeScript nutzt, bekommt vollständige Typsicherheit aus der Store-Definition ohne zusätzliche Deklarationen.
Pinia State Management — Das Wichtigste auf einen Blick
Store-Struktur
Setup-Style mit ref(), computed() und Funktionen — direkt aus der Composition API, vollständig typisiert ohne manuelle Interfaces.
Actions & Fehler
Async Actions mit isLoading und error im State. try/catch/finally sichert korrekte Flag-Rücksetzung auch bei Fehlern.
Komposition
Stores importieren andere Stores direkt — kein Root-Store, kein Namespacing, keine String-basierten Dispatch-Aufrufe.
Plugins & Persistenz
pinia-plugin-persistedstate für localStorage. $onAction-Hooks für Logging und Analytics zentral im Plugin definieren.