Modul 6: State – Pinia

Warum Pinia?

Globales State-Management in Vue 3 – und warum Props-Drilling irgendwann nicht mehr reicht.

Das Problem: State zwischen Komponenten teilen

In kleinen Apps reichen props und emit völlig aus. Aber sobald mehrere Ebenen tief verschachtelte Komponenten auf dieselben Daten zugreifen müssen, wird es schnell unübersichtlich.

Props-Drilling – das Problem
<!-- App.vue -->
<template>
  <Header :user="user" />
  <Sidebar :user="user">           <!-- braucht user gar nicht -->
    <UserProfile :user="user" />    <!-- braucht user wirklich -->
  </Sidebar>
</template>

<!-- Sidebar.vue – leitet user nur durch -->
<template>
  <nav>
    <slot />                         <!-- user muss als Prop durchgeschleift werden -->
  </nav>
</template>

Die Sidebar braucht den User gar nicht – sie reicht ihn nur durch. Bei drei, vier Ebenen wird das schnell unhaltbar.

Drei Ansätze im Vergleich

🔽 Props Drilling

Daten von Eltern an Kind weitergeben – einfach, aber skaliert nicht. Jede Zwischenkomponente muss Props durchschleifen.

📡 Events nach oben

Kinder emittieren Events – Eltern reagieren. Funktioniert für direkte Eltern-Kind-Beziehungen, aber nicht quer durch den Komponentenbaum.

🏪 Globaler Store

Ein zentraler Speicher, auf den jede Komponente direkt zugreifen kann. Genau das ist Pinia.

Datenfluss mit Pinia

Architektur-Überblick
Interaktiv
┌─────────────────────────────────────────────┐
│                Pinia Store                  │
│                                             │
│  state: { user, cart, theme }               │
│  getters: { cartTotal, isLoggedIn }         │
│  actions: { login(), addToCart() }          │
└──────────┬──────────┬──────────┬────────────┘
           │          │          │
     ┌─────▼──┐  ┌────▼───┐  ┌──▼──────┐
     │ Header │  │ Sidebar│  │ CartPage│
     │        │  │        │  │         │
     │ user ✓ │  │ cart ✓ │  │ cart ✓  │
     │ theme ✓│  │ theme ✓│  │ total ✓ │
     └────────┘  └────────┘  └─────────┘

     Jede Komponente liest direkt aus dem Store.
     Keine Props nötig – alles reaktiv.

Pinia vs. Vuex – Warum Pinia gewonnen hat

Vuex war der offizielle State-Manager für Vue 2. Pinia wurde als leichtgewichtige Alternative entwickelt und ist seit Vue 3 die offizielle Empfehlung.

FeatureVuexPinia
MutationsPflicht (commit)Keine – direkt ändern ✅
TypeScriptNachgerüstet, umständlichVon Grund auf typisiert ✅
DevToolsJaJa (besser integriert) ✅
Composition APIUmständlichNativer Support ✅
BoilerplateViel (mutations, actions, …)Minimal ✅
Bundle-Größe~10 KB~1.5 KB ✅
💡

Klare Empfehlung

Vuex 5 wird nicht mehr kommen. Pinia ist Vuex 5. Für neue Projekte gibt es keinen Grund mehr, Vuex zu verwenden.

Kernkonzepte von Pinia

🏪 Store

Ein reaktiver Container für zusammengehörigen State. Du definierst einen Store pro Domäne: useUserStore, useCartStore, useThemeStore.

📦 State

Die eigentlichen Daten – reaktive Variablen im Store. Vergleichbar mit data() in der Options API.

🔍 Getters

Berechnete Werte basierend auf dem State – wie computed(). Werden automatisch gecacht und nur bei Änderung neu berechnet.

⚡ Actions

Funktionen, die den State verändern – synchron oder asynchron. Hier gehört deine Business-Logik hin: API-Calls, Validierungen, Berechnungen.

Installation & Setup

ℹ️

In Nuxt 3 bereits integriert

Nuxt 3 hat ein offizielles Pinia-Modul. Du musst Pinia nicht manuell in die App einbinden – das Modul erledigt alles automatisch.

Terminal
# Pinia + Nuxt-Modul installieren
npx nuxi module add pinia

# Oder manuell:
npm install pinia @pinia/nuxt
nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',  // Pinia automatisch konfiguriert
  ],
})

Das war's. Kein createPinia(), kein app.use() – das Nuxt-Modul erledigt das automatisch. Du kannst sofort Stores definieren.

Für Rails-Entwickler

🛤️

Rails-Vergleich: State-Management

In Rails hast du mehrere Orte für „State":

  • Session/Cookies – User-bezogener State über Requests hinweg
  • Redis/Memcached – Geteilter Cache zwischen Prozessen
  • ActiveRecord – Persistenter State in der Datenbank
  • Instance-Variablen – State innerhalb eines Requests (@user)

Pinia ist am ehesten vergleichbar mit Instance-Variablen im Controller: globaler Zugriff innerhalb einer „Session" (hier: der Seiten-Lebenszyklus im Browser). Der Store lebt so lange wie die App im Browser – ähnlich wie eine Rails-Session, aber client-seitig.

Rails vs. Pinia – Konzeptvergleich
# Rails: Globaler State über verschiedene Mechanismen
# ───────────────────────────────────────────────
# Session (Browser-bezogen):
session[:current_user_id] = user.id

# Cache (prozessübergreifend):
Rails.cache.write("user_#{id}", user_data)

# Controller Instance-Variable (Request-bezogen):
class ApplicationController < ActionController::Base
  before_action :set_current_user

  def set_current_user
    @current_user = User.find(session[:current_user_id])
    # → Vergleichbar mit: const user = useUserStore()
  end
end

# Pinia-Äquivalent:
# ─────────────────
# const userStore = useUserStore()
# userStore.user         → wie @current_user
# userStore.login()      → wie set_current_user
# userStore.isLoggedIn   → wie @current_user.present?

Dein erster Store – Vorschau

So sieht ein minimaler Pinia-Store aus. Im nächsten Kapitel gehen wir das Schritt für Schritt durch.

stores/counter.ts
// stores/counter.ts
export const useCounterStore = defineStore('counter', () => {
  // State (wie ref() in Composition API)
  const count = ref(0)

  // Getter (wie computed())
  const doubleCount = computed(() => count.value * 2)

  // Action (normale Funktion)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})
💡

Setup-Syntax bevorzugt

Wir verwenden in diesem Tutorial die Setup-Syntax für Stores (Composition API Stil). Sie ist flexibler, besser typisiert und konsistent mit <script setup> in Komponenten.