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
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 – 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.
<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:
<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.
<!-- 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
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 }
})<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
export default defineNuxtRouteMiddleware((to) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
return navigateTo('/login')
}
})// 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
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 -->
<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 -->
<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>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