Wann welches Muster in Vue 3?
„Brauchen wir hier Pinia oder reicht ein Composable?" ist eine der häufigsten Architekturfragen in Vue-3-Projekten. Die Antwort entscheidet darüber, ob globaler State zentralisiert oder verteilt, sichtbar oder verborgen, DevTools-trackbar oder unsichtbar ist. Der falsche Ansatz kostet Monate Debugging-Aufwand, wenn die Anwendung wächst.
Inhaltsverzeichnis
- 1. Das Kernproblem: Wann ist State „global"?
- 2. Der Composable Store: reaktiver State außerhalb von Komponenten
- 3. Pinia: defineStore, State, Getters und Actions
- 4. Pinia Setup Stores: Composables als Store-Definition
- 5. DevTools-Integration: Warum Pinia sichtbarer State ist
- 6. SSR und Hydration: Pinia vs. Composable Store im Server-Rendering
- 7. Die Entscheidungsmatrix: Composable oder Pinia?
- 8. Store-Komposition: Pinia-Stores in anderen Stores nutzen
- 9. Direkter Vergleich: Composable Store vs. Pinia Store
- 10. Zusammenfassung
- 11. FAQ
1. Das Kernproblem: Wann ist State „global"?
Die Unterscheidung zwischen lokalem und globalem State ist die Grundlage jeder State-Management-Entscheidung in Vue. Lokaler State gehört einer Komponente und ist für andere Teile der Anwendung nicht relevant – ein Dropdown-Offen-Zustand, ein temporärer Eingabewert, der Lade-Indikator eines einzelnen Buttons. Globaler State ist State, auf den mehrere, voneinander unabhängige Teile der Anwendung gleichzeitig zugreifen und ihn verändern – Authentifizierung, Warenkorb, Benutzereinstellungen, Notification-Queue.
Die Entscheidung für Pinia oder ein einfaches Composable hängt primär von dieser Frage ab: Muss dieser State von unverbundenen Komponenten zur gleichen Zeit genutzt werden? Wenn ja, ist Pinia die strukturell richtige Wahl. Wenn nein – wenn also Prop-Drilling oder provide/inject den State ausreichend verteilt –, ist ein lokaler Composable-State oder ein Composable Store die leichtgewichtigere Alternative. Das falsche Muster zu wählen kostet: zu viel Pinia erzeugt unnötige Komplexität und Boilerplate, zu wenig Pinia führt zu State-Synchronisierungsproblemen zwischen Komponenten.
Ein dritter Zustand existiert dazwischen: der Composable Store. Das ist ein Vue Composable, das reaktiven State in Modulscope hält – außerhalb der Komponenteninstanz. Weil der Modulscope ein Singleton ist, teilen alle Importer denselben State. Das ist ein mächtiges, aber unsichtbares Muster, das in kleinen Projekten gut funktioniert, bei SSR aber zu Request-übergreifenden State-Kontaminierungen führen kann.
2. Der Composable Store: reaktiver State außerhalb von Komponenten
Ein Composable Store nutzt die Tatsache, dass Vue-Reaktivität nicht auf Komponenten beschränkt ist. Ein ref oder reactive, das im Modulscope einer TypeScript-Datei erstellt wird, ist reaktiv und überlebt Komponenteninstanzen. Wenn mehrere Komponenten dasselbe Composable importieren, greifen sie auf denselben Ref zu – den State im Modulscope. Das ergibt ein einfaches, leichtgewichtiges State-Sharing ohne externe Bibliothek.
Das Muster sieht aus wie ein normales Composable, hat aber reaktiven State außerhalb der Funktion. Die Funktion selbst ist dann nur noch eine Fabrik, die auf den gemeinsamen State zugreift und Methoden zur Verfügung stellt. Dieses Composable-Store-Muster funktioniert gut für State, der anwendungsweit geteilt werden muss, aber keine DevTools-Integration, keine Persistenz und keine Time-Travel-Debugging-Funktionalität braucht. Für Teams mit starken Vue-DevTools-Workflows ist die Unsichtbarkeit des Composable-Store-States ein echtes Problem.
Das kritische Limit des Composable-Store-Musters ist SSR. In einer Server-Rendering-Umgebung (Nuxt) wird der Modulscope pro Server-Prozess, nicht pro Request, initialisiert. Das bedeutet: State aus einer vorherigen Anfrage kann in die nächste Anfrage durchbluten. Pinia löst dieses Problem strukturell durch Request-scope State – jede SSR-Request bekommt eine frische Pinia-Instanz.
// src/stores/useNotifications.ts
// Composable Store: module-scope reactive state (singleton — NOT SSR-safe)
import { ref, readonly } from 'vue'
interface Notification { id: number; message: string; type: 'success' | 'error' | 'info' }
// Module-scope state — shared across all imports
const notifications = ref<Notification[]>([])
let nextId = 1
// All consumers share the same reactive state
export function useNotifications() {
function add(message: string, type: Notification['type'] = 'info') {
notifications.value.push({ id: nextId++, message, type })
}
function remove(id: number) {
notifications.value = notifications.value.filter(n => n.id !== id)
}
function clear() { notifications.value = [] }
return {
notifications: readonly(notifications), // expose as readonly
add,
remove,
clear,
}
}
3. Pinia: defineStore, State, Getters und Actions
Pinia ist die offizielle State-Management-Lösung für Vue 3 und ersetzt Vuex. Der zentrale Baustein ist defineStore(), das einen eindeutigen Store-Namen (ID), den initialen State als Funktion, Getters als computed-ähnliche Properties und Actions als Methoden entgegennimmt. Die Store-ID ist nicht nur ein Name – sie ist der Schlüssel in der globalen Pinia-Instanz und der Identifier in den Vue DevTools.
Der Options-Store-Stil von Pinia ähnelt der Vue 2 Options API: state, getters und actions als separate Objekte. Das ist für Teams, die von Vuex kommen, der einfachere Einstieg. Innerhalb von Actions hat man direkten Zugriff auf this – den Store selbst – und kann andere Stores mit useOtherStore() aufrufen. Pinia unterstützt Plugin-System, sodass Persistenz, Reset-Funktionalität oder Logging einmal als Plugin implementiert und für alle Stores aktiviert werden kann.
Ein entscheidender Vorteil von Pinia gegenüber dem Composable-Store-Muster: vollständige TypeScript-Inferenz ohne manuelle Typangaben. Der State-Typ wird aus der state()-Funktion inferiert, Getter-Typen aus den Return-Types der Getter-Funktionen, Action-Parameter aus den Action-Signaturen. Das DevTools-Panel zeigt alle aktiven Pinia-Stores mit ihrem aktuellen State, ihren Mutations und Time-Travel-Snapshots – ein enormer Debugging-Vorteil gegenüber dem unsichtbaren Modulscope-State.
4. Pinia Setup Stores: Composables als Store-Definition
Pinia unterstützt seit Version 2 einen zweiten Definitionsstil: Setup Stores. Statt des Options-Objekts übergibt man defineStore eine Funktion, die genau wie ein Vue Composable aufgebaut ist – mit ref, computed, watch und expliziten Return-Statements. Das ist das Beste aus beiden Welten: die Struktur von Composables kombiniert mit der Pinia-Infrastruktur (DevTools, Plugins, SSR-Safety).
Setup Stores ermöglichen, bestehende Composables in einen Pinia-Store zu "heben". Ein useAuth-Composable, das für ein Team groß genug geworden ist um DevTools-Integration zu rechtfertigen, kann eins-zu-eins in einen Pinia-Setup-Store konvertiert werden – der Code bleibt fast identisch. Das macht Setup Stores zur bevorzugten Pinia-API für Teams, die bereits mit Composables vertraut sind. Die einzige semantische Änderung: alles, was im Return-Statement des Setup Stores zurückgegeben wird, ist Teil des öffentlichen Store-Interface.
// src/stores/useAuthStore.ts
// Pinia Setup Store — composable syntax with full DevTools integration
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
// State as refs — shown in Vue DevTools
const user = ref<{ id: number; name: string; email: string } | null>(null)
const token = ref<string | null>(null)
const loading = ref(false)
// Getters as computed — cached and reactive
const isAuthenticated = computed(() => token.value !== null)
const displayName = computed(() => user.value?.name ?? 'Guest')
// Actions — can be async, tracked in DevTools
async function login(email: string, password: string) {
loading.value = true
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: { 'Content-Type': 'application/json' },
})
const data = await response.json()
token.value = data.token
user.value = data.user
} finally {
loading.value = false
}
}
function logout() {
user.value = null
token.value = null
}
return { user, token, loading, isAuthenticated, displayName, login, logout }
})
5. DevTools-Integration: Warum Pinia sichtbarer State ist
Vue DevTools zeigen jeden aktiven Pinia-Store mit seinem vollständigen State, allen Getters und einem Aktions-Log in Echtzeit. Das ermöglicht im Debugging-Fall sofort zu sehen, was der aktuelle Auth-State ist, welche Produkte im Warenkorb liegen und welche Action zuletzt ausgeführt wurde – ohne Console-Logs oder Breakpoints. Time-Travel-Debugging erlaubt, zu einem früheren State zurückzuspringen und die Anwendung ab diesem Punkt erneut zu beobachten.
Composable Stores sind für DevTools unsichtbar. Ein useNotifications-Composable mit Modulscope-State erscheint nicht im DevTools-Panel. Das macht Debugging schwieriger, denn man muss manuell console.log-Anweisungen hinzufügen, um den State zu inspizieren. Für kleine, gut verstandene State-Einheiten ist das akzeptabel. Für kritischen Anwendungsstate – Authentifizierung, Warenkorb, Checkout-Schritte – ist die DevTools-Sichtbarkeit von Pinia ein entscheidender Vorteil, der Debugging-Zeit in der Produktion reduziert.
Das Pinia-Plugin-System verstärkt den DevTools-Vorteil weiter: das pinia-plugin-persistedstate-Plugin persistiert ausgewählte Store-Properties in LocalStorage automatisch, mit vollständiger DevTools-Integration. Ein Composable-Store müsste Persistenz manuell implementieren, ohne die DevTools-Synchronisation zu bekommen. Für Anwendungen mit komplexem persistiertem State ist Pinia daher klar vorzuziehen.
6. SSR und Hydration: Pinia vs. Composable Store im Server-Rendering
In Nuxt-3-Anwendungen oder anderen SSR-Setups ist die Wahl zwischen Composable Store und Pinia keine Stilfrage mehr, sondern eine Korrektheitsfrage. Server-Prozesse verarbeiten multiple Requests gleichzeitig. Modulscope-State – also der State in einem Composable Store – ist Request-übergreifend geteilt. Ein User A, der sich anmeldet, könnte theoretisch State in den Request von User B durchbluten lassen. Das ist ein kritischer Sicherheitsfehler, kein Performanceproblem.
Pinia löst SSR durch seine Architektur: Nuxt erstellt für jeden Request eine neue Pinia-Instanz via createPinia(). Alle Stores in diesem Request verwenden diese Instanz und sind voneinander isoliert. Nach dem Rendering serialisiert Pinia den State und schickt ihn an den Client. Im Client hydratiert Pinia die Stores aus dem serialisierten State, sodass kein erneuter API-Call nötig ist. Dieses Hydrations-Protokoll ist ein ausgereiftes Feature, das bei Composable Stores manuell implementiert werden müsste.
7. Die Entscheidungsmatrix: Composable oder Pinia?
Die Entscheidung zwischen Composable Store und Pinia lässt sich auf fünf Fragen reduzieren. Wenn eine der Fragen mit Ja beantwortet wird, ist Pinia die bessere Wahl: Wird der State von unverbundenen Komponenten auf verschiedenen Routen geteilt? Soll der State in Vue DevTools sichtbar sein? Wird SSR verwendet? Ist Persistenz (LocalStorage, Cookie) ein Requirement? Müssen Actions in DevTools geloggt werden?
Wenn alle fünf Fragen mit Nein beantwortet werden, ist ein einfaches Composable oder ein Composable Store ausreichend. Das gilt typischerweise für: UI-State innerhalb einer Seite oder eines Features, Lade-Indikatoren und Fehler-State für spezifische Komponenten, temporären Form-State, der nicht persistiert werden muss, und Zustand, der nur innerhalb eines Komponentenbaums geteilt wird und mit provide/inject verteilt werden kann.
| Kriterium | Composable Store | Pinia Store | Empfehlung |
|---|---|---|---|
| SSR / Nuxt | Request-Kontamination möglich | Request-isoliert | Pinia |
| DevTools-Sichtbarkeit | Unsichtbar | Vollständig integriert | Pinia |
| Boilerplate | Minimal | defineStore + ID | Composable |
| Persistenz | Manuell | Via Plugin automatisch | Pinia |
| Testing | Einfach | createPinia() in Tests nötig | Beide gut möglich |
8. Store-Komposition: Pinia-Stores in anderen Stores nutzen
Eine der leistungsfähigsten Features von Pinia ist die Store-Komposition: Ein Store kann andere Stores mit useOtherStore() innerhalb seiner Actions oder Getters aufrufen. Das ermöglicht granulare, thematisch klare Stores – useCartStore und useProductStore –, die in einem useCheckoutStore zusammenarbeiten, ohne alle Logik in einen monolithischen Store zu packen. Diese Kompositionsfähigkeit fehlt bei Composable Stores nicht grundsätzlich, ist aber ohne die Pinia-Infrastruktur schwieriger sauber zu koordinieren.
Die Konvention bei Pinia-Store-Komposition: Einen anderen Store niemals auf Modulebene importieren und instanziieren, sondern immer lazy innerhalb einer Action oder eines Getters aufrufen. Das vermeidet zirkuläre Abhängigkeiten und stellt sicher, dass Pinia vollständig initialisiert ist, bevor der abhängige Store aufgerufen wird. In Setup Stores ist die Komposition noch natürlicher – man ruft useAuthStore() direkt in der Setup-Funktion auf und hat Zugriff auf alle State-Properties des Auth-Stores als reaktive Refs.
// src/stores/useCheckoutStore.ts
// Store composition: checkout depends on cart and auth stores
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useCartStore } from './useCartStore'
import { useAuthStore } from './useAuthStore'
export const useCheckoutStore = defineStore('checkout', () => {
const cart = useCartStore() // compose other Pinia stores
const auth = useAuthStore()
const step = ref<'address' | 'payment' | 'confirm'>('address')
const processing = ref(false)
// Getter using composed store state
const canCheckout = computed(() =>
auth.isAuthenticated && cart.items.length > 0 && !processing.value
)
async function submitOrder() {
if (!canCheckout.value) return
processing.value = true
try {
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ items: cart.items, userId: auth.user?.id }),
headers: { 'Content-Type': 'application/json' },
})
cart.clear() // action from composed store
step.value = 'confirm'
} finally {
processing.value = false
}
}
return { step, processing, canCheckout, submitOrder }
})
9. Direkter Vergleich: Composable Store vs. Pinia Store
In der täglichen Praxis arbeiten Pinia Stores und Composable Stores nicht gegeneinander, sondern ergänzen sich. Die Entscheidung ist kontextabhängig und sollte pro State-Einheit, nicht pro Projekt, getroffen werden. Ein Vue-3-Projekt kann gleichzeitig Pinia für globalen Anwendungsstate und einfache Composables für komponentenlokalen State verwenden – ohne Widerspruch.
Der entscheidende praktische Unterschied für mittlere und große Teams: Pinia-Stores sind sofort dokumentiert durch ihre ID und ihre Struktur. Ein neues Teammitglied sieht in den DevTools sofort, welche Stores existieren und was drin ist. Ein Composable Store ist nur durch Lesen des Source-Codes zu entdecken. Für Teams mit mehr als drei bis vier Entwicklern ist die Explizitheit von Pinia in der Regel den minimalen Mehraufwand wert.
Mironsoft
Vue-3-Architektur, State-Management und Pinia-Setup für komplexe Anwendungen
State-Management-Architektur für euer Vue-3-Projekt?
Wir analysieren eure bestehende State-Struktur, identifizieren falsch eingesetzte Composable Stores und migrieren kritischen State zu Pinia – mit DevTools-Integration, SSR-Safety und Plugin-Setup.
State-Audit
Bestehende State-Struktur analysieren und kritische Composable-Store-Risiken in SSR identifizieren
Pinia-Migration
Vuex oder Composable Stores zu Pinia-Setup-Stores migrieren mit vollständigen Tests
Plugin-Setup
Persistenz, Logging und Reset via Pinia-Plugins einrichten und dokumentieren
10. Zusammenfassung
Die Wahl zwischen Pinia und einem Composable Store ist keine dogmatische Entscheidung, sondern eine Abwägung nach konkreten Anforderungen. Composable Stores sind ideal für State, der nicht SSR-sicher sein muss, keine DevTools-Sichtbarkeit braucht und nur innerhalb klar abgegrenzter Feature-Grenzen geteilt wird. Pinia ist die richtige Wahl für kritischen Anwendungsstate, SSR-Umgebungen, Teams mit DevTools-basierten Workflows und Anwendungen, die Persistenz oder Plugin-Infrastruktur benötigen.
Setup Stores in Pinia machen die Migration von Composable Stores trivial: Die Syntax ist nahezu identisch, nur der Wrapper defineStore('id', () => { ... }) kommt hinzu. Teams, die bereits mit Vue Composables vertraut sind, können diese Vertrautheit direkt auf Pinia Setup Stores übertragen. Store-Komposition ermöglicht granulare, testbare Stores statt eines Monolithen. Das Ergebnis ist ein State-Management, das mit der Anwendungskomplexität wächst, ohne die Wartbarkeit zu opfern.
Composable Stores vs. Pinia — Das Wichtigste auf einen Blick
Composable Store
Modulscope-State ohne Bibliothek. Geeignet für nicht-SSR-Kontext, kleine Teams, State ohne DevTools-Anforderung. Kein SSR einsetzen.
Pinia Store
Request-isoliert, DevTools-sichtbar, Plugin-fähig. Pflicht bei SSR, globalem State und Teams mit Debugging-Anforderungen via DevTools.
Setup Stores
Composable-Syntax mit Pinia-Infrastruktur. Beste Migration von Composable Stores – nahezu identische Syntax, volle DevTools-Integration.
Store-Komposition
Pinia-Stores können andere Stores aufrufen. Granulare, thematische Stores statt Monolith. useCartStore in useCheckoutStore verwenden.