Modul 6: State – Pinia

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.

Die Plugin-Grundstruktur
// 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.

Terminal
# Für Nuxt (empfohlen):
npx nuxi module add pinia-plugin-persistedstate

# Oder manuell:
npm install pinia-plugin-persistedstate
nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
    'pinia-plugin-persistedstate/nuxt',  // ← Plugin-Modul hinzufügen
  ],
})
stores/settings.ts – persistierter Store
// 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
// 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 (Nuxt)
// 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
// 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
// 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)     // ✅ typsicher
💡

TypeScript-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.

$subscribe – State-Änderungen beobachten
// 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
)
$onAction – Actions beobachten
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
// 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'],
  },
})
components/ThemeSwitcher.vue
<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>
Theme-Persistierung (Simulation)
Interaktiv
🌙theme: dark

Dunkles Theme aktiv. Wird im localStorage gespeichert und beim nächsten Besuch automatisch wiederhergestellt.

Simulierter localStorage-Eintrag:

pinia-theme → {"theme":"dark","fontSize":"md"}
Schriftgröße:

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