<v/>
{ }
Vue 3 · Pinia · State Management · Composables
Composable Stores vs. Pinia Stores
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.

14 Min. Lesezeit Pinia · defineStore · useStore · reactive · provide/inject Vue 3.x · Pinia 2.x · TypeScript

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.

11. FAQ: Composable Stores vs. Pinia Stores

1Composable Store vs. Pinia: der Kern-Unterschied?
Composable Store: Modulscope-State, DevTools-unsichtbar, nicht SSR-sicher. Pinia: Request-isoliert, DevTools-sichtbar, plugin-fähig.
2Ist Pinia immer besser?
Nein. Für Feature-lokalen State ohne SSR und DevTools-Anforderung ist Composable Store leichtgewichtiger und boilerplate-ärmer.
3Warum Composable Stores in SSR problematisch?
Modulscope ist Request-übergreifend geteilt. State von User A kann zu User B durchbluten. Pinia ist Request-isoliert durch createPinia() pro Request.
4Was sind Pinia Setup Stores?
Composable-Syntax als Store-Definition: defineStore('id', () => { ... }). Volle Pinia-Infrastruktur mit vertrauter ref/computed-Syntax.
5Store-Komposition in Pinia?
useOtherStore() in Actions und Gettern aufrufen. Nie auf Modulebene instanziieren – zirkuläre Abhängigkeiten vermeiden.
6Pinia Persistenz?
pinia-plugin-persistedstate serialisiert State in LocalStorage mit DevTools-Integration. Kein manueller Code nötig.
7Pinia Stores testen?
createPinia() + setActivePinia() pro Test. Frische Instanz per Test. State setzen, Actions aufrufen, Getters prüfen.
8Vuex zu Pinia migrieren?
Ja – Pinia ist offizieller Vuex-Nachfolger für Vue 3. Leichter, typsicherer, besser integriert. Iterativ pro Store migrieren.
9$reset() in Setup Stores?
Nur in Options Stores eingebaut. In Setup Stores eigene reset-Action implementieren oder das $reset-Plugin verwenden.
10Pinia und Composable Stores mischen?
Ja – empfohlenes Muster. Pinia für globalen State, Composable Store für Feature-lokalen State ohne SSR. Koexistieren problemlos.