<v/>
{ }
Vue 3 · Pinia · State Management · Composition API
State Management mit Pinia richtig aufbauen
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.

18 Min. Lesezeit defineStore · Actions · Getters · Store-Komposition · DevTools Vue 3.4+ · Pinia 2.x · TypeScript

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.

11. FAQ: State Management mit Pinia

1Was ist der Unterschied zwischen Pinia und Vuex?
Pinia verzichtet auf Mutations, unterstützt TypeScript nativ, hat kein Namespacing und nutzt die Composition API. Stores importieren sich gegenseitig direkt — kein Root-Store nötig.
2Options-Style oder Setup-Style?
Setup-Style für TypeScript-Projekte — Typen werden aus ref() und computed() abgeleitet. Options-Style für Teams, die von Vuex migrieren. Beide Stile können gemischt werden.
3Store-zu-Store-Kommunikation?
Store direkt importieren und dessen State und Actions nutzen. Kein Root-Store, kein Namespacing. Zirkelabhängigkeiten durch dritten Store oder Composable auflösen.
4State im localStorage speichern?
pinia-plugin-persistedstate installieren und mit pinia.use() registrieren. In der Store-Definition persist: true oder Konfigurationsobjekt mit paths angeben.
5Pinia mit Nuxt 3 verwenden?
@pinia/nuxt Modul installieren. Erstellt pro SSR-Request eine neue Pinia-Instanz und hydratisiert den State auf dem Client automatisch.
6Was ist $patch()?
Atomarer Update mehrerer State-Properties als ein einziger DevTools-Eintrag. Für komplexe Updates besser als einzelne direkte Zuweisungen.
7Pinia-Stores unit-testen?
createPinia() pro Test, setActivePinia(). Actions direkt aufrufen, API mit vi.mock() oder MSW mocken. Nach dem Test pinia.$dispose().
8storeToRefs() — wann nötig?
Beim Destructuren von State und Getters. Direktes Destructuring verliert die Reaktivität. Actions können ohne storeToRefs() destructuriert werden.
9Pinia-Plugins — wann sinnvoll?
Für Persistenz, Logging, Analytics und Error-Reporting — Cross-Cutting-Concerns, die alle Stores betreffen und nicht in einzelnen Store-Actions definiert gehören.
10Zu viele granulare Stores vermeiden?
Stores nach Domänen-Bounded-Contexts strukturieren. Lokaler Komponenten-State bleibt in ref() in der Komponente. Ein Store pro Entität ist meistens zu granular.