Plugins & Persistierung
Pinia erweitern mit Plugins – von localStorage-Persistierung bis globalem Logging.
Was sind Pinia Plugins?
Pinia Plugins sind Funktionen, die jeden Store erweitern. Sie werden einmal registriert und laufen automatisch für jeden Store, der erstellt wird. Damit kannst du Querschnittsfunktionalität zentral definieren: Logging, Persistierung, Error Tracking, Undo/Redo.
// plugins/mein-plugin.ts
import type { PiniaPlugin } from 'pinia'
// Ein Plugin ist eine Funktion, die einen Context erhält
const meinPlugin: PiniaPlugin = (context) => {
// context.store → die Store-Instanz
// context.app → die Vue-App-Instanz
// context.pinia → die Pinia-Instanz
// context.options → die Store-Optionen
console.log(`Store erstellt: ${context.store.$id}`)
// Optional: Properties zum Store hinzufügen
return {
createdAt: new Date(),
}
}
// In der App registrieren (ohne Nuxt)
// const pinia = createPinia()
// pinia.use(meinPlugin)Rails-Vergleich: Plugins
Pinia Plugins sind vergleichbar mit ActiveRecord Concerns oder Rails Middleware. Wie ein Concern Verhalten in jedes Model injiziert (include Searchable), erweitert ein Pinia Plugin jeden Store mit zusätzlicher Funktionalität – ohne den Store selbst zu ändern.
pinia-plugin-persistedstate
Das beliebteste Pinia-Plugin. Es speichert Store-State automatisch in localStorage (oder sessionStorage, Cookies, etc.) und stellt ihn beim Seitenneuladen wieder her.
# Für Nuxt (empfohlen):
npx nuxi module add pinia-plugin-persistedstate
# Oder manuell:
npm install pinia-plugin-persistedstate// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@pinia/nuxt',
'pinia-plugin-persistedstate/nuxt', // ← Plugin-Modul hinzufügen
],
})// stores/settings.ts
export const useSettingsStore = defineStore('settings', () => {
const theme = ref<'dark' | 'light' | 'system'>('system')
const language = ref('de')
const fontSize = ref<'sm' | 'md' | 'lg'>('md')
const sidebarOpen = ref(true) // ← soll NICHT persistiert werden
function setTheme(newTheme: typeof theme.value) {
theme.value = newTheme
}
return {
theme, language, fontSize, sidebarOpen,
setTheme,
}
}, {
persist: {
// Nur bestimmte Felder persistieren
pick: ['theme', 'language', 'fontSize'],
// Storage wählen (default: localStorage)
storage: persistedState.localStorage,
},
})Selektive Persistierung
Du musst nicht den gesamten State persistieren. Mit pick kannst du gezielt Felder auswählen – z.B. Theme und Sprache speichern, aber temporäre UI-States nicht.
Custom Plugin: Logging
Ein eigenes Plugin zu schreiben ist einfach. Hier ein Logger, der alle State-Änderungen in die Konsole schreibt – perfekt für Debugging.
// plugins/pinia-logger.ts
import type { PiniaPlugin } from 'pinia'
export const piniaLogger: PiniaPlugin = ({ store }) => {
// State-Änderungen loggen
store.$subscribe((mutation, state) => {
console.group(`🏪 [${store.$id}] State-Änderung`)
console.log('Typ:', mutation.type) // 'direct' | 'patch object' | 'patch function'
console.log('Events:', mutation.events) // Details der Änderung
console.log('Neuer State:', JSON.parse(JSON.stringify(state)))
console.groupEnd()
})
// Actions loggen
store.$onAction(({ name, args, after, onError }) => {
const startTime = performance.now()
console.log(`⚡ [${store.$id}] Action: ${name}`, args)
after((result) => {
const duration = (performance.now() - startTime).toFixed(1)
console.log(`✅ [${store.$id}] ${name} fertig (${duration}ms)`, result)
})
onError((error) => {
console.error(`❌ [${store.$id}] ${name} fehlgeschlagen:`, error)
})
})
}// plugins/pinia-logger.client.ts
// .client.ts = läuft nur im Browser
import { piniaLogger } from '~/plugins/pinia-logger'
export default defineNuxtPlugin(({ $pinia }) => {
if (false) {
// Nur im Development-Modus loggen
$pinia.use(piniaLogger)
}
}) Beachte das .client.ts-Suffix: Dieses Plugin läuft nur im Browser, nicht beim Server-Side Rendering. Das ist wichtig, weil console.group() auf dem Server keinen Sinn ergibt.
Custom Plugin: Error Handling
Dieses Plugin fängt Fehler in allen Actions ab und meldet sie an einen zentralen Error-Tracker. Kein Try-Catch in jeder einzelnen Action nötig.
// plugins/pinia-error-handler.ts
import type { PiniaPlugin } from 'pinia'
export const piniaErrorHandler: PiniaPlugin = ({ store }) => {
store.$onAction(({ name, onError }) => {
onError((error) => {
// An Error-Tracking-Service senden
console.error(`[Store: ${store.$id}] Action "${name}" fehlgeschlagen:`, error)
// Optional: An Sentry, Bugsnag etc. melden
// useSentry().captureException(error, {
// tags: { store: store.$id, action: name },
// })
// Fehler weiterwerfen – Komponenten können trotzdem reagieren
throw error
})
})
}
// Registrierung in Nuxt:
// plugins/error-handler.ts
// export default defineNuxtPlugin(({ $pinia }) => {
// $pinia.use(piniaErrorHandler)
// })Fehler nicht verschlucken
Das Plugin loggt Fehler und gibt sie weiter (throw error). So können Komponenten trotzdem mit try/catch auf Fehler reagieren. Ein Plugin sollte Fehler beobachten, nicht unterdrücken.
Stores mit Plugin-Properties erweitern
Plugins können neue Properties zu jedem Store hinzufügen. Das ist nützlich für geteilte Helfer-Funktionen oder Konfigurationswerte.
// plugins/pinia-helpers.ts
import type { PiniaPlugin } from 'pinia'
export const piniaHelpers: PiniaPlugin = ({ store }) => {
// Neue Property zu jedem Store hinzufügen
return {
// Zeitstempel der Erstellung
createdAt: new Date(),
// Helper-Methode: State als JSON exportieren
toJSON() {
return JSON.parse(JSON.stringify(store.$state))
},
// Helper: Prüfen ob Store Daten geladen hat
get hasData() {
return Object.values(store.$state).some(
(v) => v !== null && v !== undefined && v !== ''
)
},
}
}
// TypeScript: Typen für die neuen Properties deklarieren
declare module 'pinia' {
export interface PiniaCustomProperties {
createdAt: Date
toJSON: () => Record<string, unknown>
readonly hasData: boolean
}
}
// Verwendung:
// const userStore = useUserStore()
// console.log(userStore.createdAt) // ✅ typsicher
// console.log(userStore.toJSON()) // ✅ typsicher
// console.log(userStore.hasData) // ✅ typsicherTypeScript-Augmentation
Erweitere das PiniaCustomProperties-Interface, damit TypeScript die neuen Properties kennt. Ohne das bekommst du Fehler beim Zugriff.
$subscribe & $onAction
Auch ohne Plugin kannst du Store-Änderungen und Actions beobachten – direkt in Komponenten oder Composables.
// In einer Komponente oder Composable
const cartStore = useCartStore()
// State-Änderungen beobachten
cartStore.$subscribe((mutation, state) => {
console.log('Cart geändert:', mutation.type)
console.log('Neue Items:', state.items)
// Beispiel: Analytics-Event senden
// trackEvent('cart_updated', { itemCount: state.items.length })
})
// Option: Auch nach Komponenten-Unmount weiterlaufen
cartStore.$subscribe(
(mutation, state) => { /* ... */ },
{ detached: true } // ← überlebt Komponenten-Lifecycle
)const cartStore = useCartStore()
// Actions beobachten – vor, nach und bei Fehlern
cartStore.$onAction(({ name, store, args, after, onError }) => {
// VOR der Action
console.log(`→ ${name} wird aufgerufen mit:`, args)
// NACH erfolgreicher Ausführung
after((result) => {
console.log(`✓ ${name} abgeschlossen. Ergebnis:`, result)
})
// BEI Fehler
onError((error) => {
console.error(`✗ ${name} fehlgeschlagen:`, error)
})
})
// Praktisches Beispiel: Automatisches Speichern nach Änderungen
const formStore = useFormStore()
formStore.$onAction(({ name, after }) => {
after(() => {
if (['updateField', 'addItem', 'removeItem'].includes(name)) {
// Auto-Save nach bestimmten Actions
formStore.saveToServer()
}
})
})Rails-Vergleich: Callbacks
$subscribe ist wie ActiveRecord Callbacks (after_save, after_update) – du reagierst auf Datenänderungen. $onAction ist eher wie around_action in Controllern – du wrappst die Ausführung und kannst vor, nach und bei Fehlern eingreifen.
Demo: Theme mit localStorage
Ein praktisches Beispiel: Ein Theme-Store, der die Einstellungen des Nutzers im localStorage speichert und beim nächsten Besuch wiederherstellt.
// stores/theme.ts
export const useThemeStore = defineStore('theme', () => {
const theme = ref<'dark' | 'light' | 'system'>('dark')
const fontSize = ref<'sm' | 'md' | 'lg'>('md')
const isDark = computed(() => {
if (theme.value === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
return theme.value === 'dark'
})
const fontSizeClass = computed(() => ({
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
}[fontSize.value]))
function setTheme(newTheme: typeof theme.value) {
theme.value = newTheme
// CSS-Klasse am <html> aktualisieren
document.documentElement.classList.toggle('dark', isDark.value)
}
return { theme, fontSize, isDark, fontSizeClass, setTheme }
}, {
persist: {
pick: ['theme', 'fontSize'],
},
})<script setup lang="ts">
import { storeToRefs } from 'pinia'
const themeStore = useThemeStore()
const { theme, fontSize, isDark } = storeToRefs(themeStore)
</script>
<template>
<div class="flex items-center gap-4">
<!-- Theme-Buttons -->
<div class="flex gap-1 rounded-lg bg-gray-800/50 p-1">
<button
v-for="opt in ['dark', 'light', 'system'] as const"
:key="opt"
:class="theme === opt ? 'bg-white/10' : ''"
class="px-3 py-1 rounded-md text-sm"
@click="themeStore.setTheme(opt)"
>
{{ opt === 'dark' ? '🌙' : opt === 'light' ? '☀️' : '💻' }}
</button>
</div>
<!-- Status -->
<span class="text-sm text-gray-400">
Aktuell: {{ isDark ? 'Dunkel' : 'Hell' }}
<!-- Wird nach Seiten-Reload automatisch wiederhergestellt -->
</span>
</div>
</template>Dunkles Theme aktiv. Wird im localStorage gespeichert und beim nächsten Besuch automatisch wiederhergestellt.
Simulierter localStorage-Eintrag:
pinia-theme → {"theme":"dark","fontSize":"md"}Beliebte Pinia Plugins
📦 pinia-plugin-persistedstate
Automatische Persistierung in localStorage, sessionStorage oder Cookies. Nuxt-Modul verfügbar.
🔄 pinia-orm
ORM-Layer für Pinia – definiere Models mit Relationships, ähnlich wie ActiveRecord. Ideal für komplexe Datenstrukturen.
↩️ pinia-undo
Undo/Redo-Funktionalität für Stores. Speichert State-Historie und ermöglicht Zeitreisen.
🧪 @pinia/testing
Offizielles Test-Utility. Erstellt isolierte Store-Instanzen für Unit-Tests mit initialem State.
Zusammenfassung
- ✅ Plugins erweitern jeden Store – einmal registrieren, überall aktiv
- ✅ pinia-plugin-persistedstate für localStorage/Cookie-Persistierung
- ✅ Custom Plugins für Logging, Error Handling, Metriken
- ✅ $subscribe() beobachtet State-Änderungen (wie ActiveRecord Callbacks)
- ✅ $onAction() beobachtet Actions (wie around_action in Rails)
- ✅ TypeScript-Augmentation für typsichere Plugin-Properties