Modul 6: State – Pinia

Stores definieren

Setup Stores, Option Stores und wie du Stores in Komponenten verwendest.

Setup Stores (Composition API Stil)

Die Setup-Syntax ist die empfohlene Art, Stores zu definieren. Sie funktioniert genau wie <script setup> in Komponenten: ref() wird zu State, computed() zu Gettern, und normale Funktionen zu Actions.

stores/user.ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  // State → ref()
  const name = ref('')
  const email = ref('')
  const isLoggedIn = ref(false)

  // Getter → computed()
  const displayName = computed(() =>
    name.value || email.value || 'Gast'
  )

  // Action → Funktion
  async function login(credentials: { email: string; password: string }) {
    const response = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials,
    })
    name.value = response.name
    email.value = response.email
    isLoggedIn.value = true
  }

  function logout() {
    name.value = ''
    email.value = ''
    isLoggedIn.value = false
  }

  return { name, email, isLoggedIn, displayName, login, logout }
})
💡

Namenskonvention

Store-Dateien liegen in stores/. Der Funktionsname beginnt immer mit use und endet mit Store – z.B. useUserStore. Das macht klar, dass es ein Composable ist, das einen Store zurückgibt.

Option Stores (kurz zum Vergleich)

Die Option-Syntax erinnert an die Vue Options API. Sie ist nicht falsch, aber weniger flexibel – besonders bei TypeScript und Composables.

stores/user-options.ts (Alternative)
// stores/user-options.ts – Option-Syntax
export const useUserStore = defineStore('user', {
  // State als Funktion (wie data() in Options API)
  state: () => ({
    name: '',
    email: '',
    isLoggedIn: false,
  }),

  // Getters als Objekt
  getters: {
    displayName(state) {
      return state.name || state.email || 'Gast'
    },
  },

  // Actions als Methoden
  actions: {
    async login(credentials: { email: string; password: string }) {
      const response = await $fetch('/api/auth/login', {
        method: 'POST',
        body: credentials,
      })
      this.name = response.name    // ← this statt state
      this.email = response.email
      this.isLoggedIn = true
    },
  },
})
ℹ️

Beide Syntaxen sind funktional identisch. In diesem Tutorial verwenden wir ausschließlich die Setup-Syntax, weil sie besser zu <script setup> passt und TypeScript-Inferenz einfacher ist.

Stores in Komponenten verwenden

Du rufst den Store einfach als Funktion auf. Ab diesem Moment hast du reaktiven Zugriff auf alle State-Werte, Getters und Actions.

components/UserGreeting.vue
<script setup lang="ts">
// Store importieren und aufrufen
const userStore = useUserStore()
</script>

<template>
  <!-- Direkt auf Properties zugreifen -->
  <p>Hallo, {{ userStore.displayName }}!</p>

  <!-- Actions aufrufen -->
  <button @click="userStore.logout()">Abmelden</button>

  <!-- State reaktiv lesen -->
  <span v-if="userStore.isLoggedIn">✅ Eingeloggt</span>
</template>
⚠️

Achtung: Destructuring bricht Reaktivität!

Wenn du State-Werte aus dem Store destrukturierst, verlierst du die Reaktivität. Verwende storeToRefs() für reaktive Destructuring:

Richtig destrukturieren
<script setup lang="ts">
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// ❌ FALSCH: Reaktivität geht verloren
// const { name, isLoggedIn } = userStore

// ✅ RICHTIG: storeToRefs() erhält Reaktivität
const { name, isLoggedIn, displayName } = storeToRefs(userStore)

// Actions können direkt destrukturiert werden (kein ref nötig)
const { login, logout } = userStore
</script>

<template>
  <p>{{ name }}</p>           <!-- reaktiv ✅ -->
  <p>{{ displayName }}</p>    <!-- reaktiv ✅ -->
  <button @click="logout">Abmelden</button>
</template>

Store-Reaktivität verstehen

Pinia-Stores sind vollständig reaktiv. Jede Änderung am State wird automatisch in allen Komponenten reflektiert, die den Store verwenden.

Reaktivität in Aktion
<!-- KomponenteA.vue -->
<script setup lang="ts">
const counter = useCounterStore()
</script>
<template>
  <p>Count: {{ counter.count }}</p>
  <button @click="counter.increment()">+1</button>
</template>

<!-- KomponenteB.vue – ganz woanders im Komponentenbaum -->
<script setup lang="ts">
const counter = useCounterStore()
</script>
<template>
  <!-- Zeigt DENSELBEN Wert – wird automatisch aktualisiert -->
  <p>Aktueller Stand: {{ counter.count }}</p>
</template>
🛤️

Rails-Vergleich: Reaktivität

In Rails musst du nach einer Datenänderung die Seite neu laden oder ActionCable/Turbo Streams verwenden. Pinia ist wie ein eingebauter Turbo Stream – Änderungen am Store werden sofort in allen Komponenten sichtbar, ohne Reload.

State zurücksetzen

Pinia bietet $reset(), um den Store auf seinen Anfangszustand zurückzusetzen. Bei Setup Stores musst du diese Methode selbst implementieren.

stores/form.ts – Reset implementieren
// stores/form.ts
export const useFormStore = defineStore('form', () => {
  const name = ref('')
  const email = ref('')
  const message = ref('')

  // Reset-Funktion manuell definieren
  function $reset() {
    name.value = ''
    email.value = ''
    message.value = ''
  }

  return { name, email, message, $reset }
})
Verwendung in einer Komponente
<script setup lang="ts">
const formStore = useFormStore()

async function submitForm() {
  await $fetch('/api/contact', {
    method: 'POST',
    body: {
      name: formStore.name,
      email: formStore.email,
      message: formStore.message,
    },
  })

  // Formular zurücksetzen
  formStore.$reset()
}
</script>

Stores außerhalb von Komponenten

Manchmal brauchst du Store-Zugriff in Middleware, Plugins oder Composables. In Nuxt funktioniert das problemlos, solange du den Aufruf im richtigen Kontext machst.

middleware/auth.ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const userStore = useUserStore()

  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    return navigateTo('/login')
  }
})
composables/useApi.ts
// composables/useApi.ts
export function useApi() {
  const userStore = useUserStore()

  async function fetchWithAuth(url: string) {
    return $fetch(url, {
      headers: {
        Authorization: userStore.token
          ? `Bearer ${userStore.token}`
          : '',
      },
    })
  }

  return { fetchWithAuth }
}
⚠️

Timing beachten

Rufe useXxxStore() nicht auf der obersten Ebene einer Datei auf (außerhalb einer Funktion). Der Store ist erst verfügbar, wenn die Nuxt-App initialisiert ist. Innerhalb von Lifecycle-Hooks, Middleware oder Composables ist das kein Problem.

Demo: Counter Store

Ein vollständiges Beispiel mit Store-Definition und zwei Komponenten, die sich den State teilen.

stores/counter.ts
// stores/counter.ts
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)

  function increment() { count.value++ }
  function decrement() { count.value-- }
  function reset() { count.value = 0 }

  return { count, doubleCount, increment, decrement, reset }
})
components/CounterDisplay.vue
<!-- components/CounterDisplay.vue -->
<script setup lang="ts">
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
</script>

<template>
  <div class="text-center">
    <p class="text-4xl font-bold">{{ count }}</p>
    <p class="text-sm text-gray-400">Doppelt: {{ doubleCount }}</p>
  </div>
</template>
components/CounterControls.vue
<!-- components/CounterControls.vue -->
<script setup lang="ts">
const counterStore = useCounterStore()
const { increment, decrement, reset } = counterStore
</script>

<template>
  <div class="flex gap-2">
    <button @click="decrement">− 1</button>
    <button @click="reset">Reset</button>
    <button @click="increment">+ 1</button>
  </div>
</template>
Reaktiver Counter
Interaktiv
0
Doppelt: 0

Beide Anzeigen (Zahl + Doppelt) reagieren reaktiv auf Änderungen.

Zusammenfassung

  • Setup Stores sind die empfohlene Syntax – konsistent mit Composition API
  • storeToRefs() für reaktives Destructuring verwenden
  • ✅ Stores sind automatisch reaktiv – Änderungen propagieren sofort
  • $reset() manuell implementieren in Setup Stores
  • ✅ Stores in Middleware und Composables nutzbar – Timing beachten