Claude Code Pinia: Vue.js State Management mit KI meistern

State Management ist eine der schwierigsten Disziplinen in der Vue.js-Entwicklung — nicht weil die Konzepte kompliziert wären, sondern weil die Entscheidungen, die man beim Aufbau von Stores trifft, sich durch die gesamte Anwendung ziehen. Ein schlecht strukturierter Store zieht schlechte Komponenten nach sich. Und Pinia — der offizielle State-Manager für Vue 3 — bietet alle Werkzeuge, um es richtig zu machen, vorausgesetzt man weiß sie zu nutzen.

Claude Code Pinia bedeutet in der Praxis: Stores entwerfen, strukturieren und refactorn in einem Bruchteil der Zeit, die man normalerweise bräuchte. Nicht weil Claude Code Pinia kennt (obwohl es das tut), sondern weil es gleichzeitig deine bestehenden Stores, deine Komponenten, deine API-Schicht und deine Typdefinitionen sieht — und Entscheidungen auf Basis des vollständigen Kontextbildes trifft.

Dieser Artikel zeigt, wie das konkret aussieht: von defineStore über Getters und Actions bis hin zu Composables und Vue DevTools-Integration.

Claude Code Mastery — Vue, Pinia, Agents auf Deutsch

Nicht nur Pinia: der Kurs zeigt, wie du Claude Code wirklich produktiv einsetzt — für moderne Frontends, autonome Agents und professionelle Workflows. Einmalig bezahlt, kein Abo.

Zum Kurs — Jetzt starten → Einmalzahlung · Kein Abo · 14 Tage Rückgaberecht

1. Warum Pinia und warum Claude Code dazu?

Pinia ist seit Vue 3.x der empfohlene State-Manager — es ersetzt Vuex, ist erheblich leichter, vollständig TypeScript-first und nutzt die Composition API nativ. Die Grundkonzepte sind schnell verstanden: state, getters, actions. Was schwierig ist, ist die Frage, wie man einen Store aufbaut, der mit der Anwendung skaliert, der testbar bleibt und der nicht zur Daten-Grube wird, in der alles landet, was in einer Komponente keinen Platz hat.

Claude Code löst genau dieses Problem. Es sieht nicht nur deinen Store in Isolation, sondern versteht den Zusammenhang: Welche Komponenten konsumieren diesen Store? Welche Actions werden häufig zusammen aufgerufen? Wo gibt es Duplikate zwischen zwei Stores, die eigentlich einer sein sollten? Und wo wird State im Store gehalten, der eigentlich lokaler Komponenten-State wäre?

Pinia vs. Vuex: Wenn du von Vuex 4 auf Pinia migrierst, ist Claude Code besonders wertvoll. Es liest deine bestehenden Vuex-Module, versteht die Struktur und schreibt äquivalente Pinia-Stores — sauber, typisiert, ohne Mutation-Boilerplate.

2. Einen ersten Pinia Store mit Claude Code erstellen

Der direkteste Einstieg: Claude Code beschreiben, was du brauchst, und den Store generieren lassen. Nicht als Black-Box — sondern mit Erklärung, warum die Struktur so ist, wie sie ist.

claude "Erstelle einen Pinia Store für die User-Authentifizierung.
Der Store soll: den aktuellen User halten, Login/Logout-Actions haben,
den Auth-Status als Getter bereitstellen und mit unserem bestehenden
api/auth.ts Service arbeiten."

Claude Code liest daraufhin api/auth.ts, versteht die vorhandenen Typen und gibt dir einen vollständig typisierten Store, der die bestehende API-Schicht nutzt:

// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { loginUser, logoutUser, fetchCurrentUser } from '@/api/auth'
import type { User } from '@/types/user'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  // Getters
  const isAuthenticated = computed(() => user.value !== null)
  const isAdmin = computed(() => user.value?.role === 'admin')
  const displayName = computed(() =>
    user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
  )

  // Actions
  async function login(email: string, password: string) {
    loading.value = true
    error.value = null
    try {
      const response = await loginUser({ email, password })
      user.value = response.user
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Login fehlgeschlagen'
      throw err
    } finally {
      loading.value = false
    }
  }

  async function logout() {
    loading.value = true
    try {
      await logoutUser()
      user.value = null
    } finally {
      loading.value = false
    }
  }

  async function initializeAuth() {
    try {
      user.value = await fetchCurrentUser()
    } catch {
      user.value = null
    }
  }

  return {
    user,
    loading,
    error,
    isAuthenticated,
    isAdmin,
    displayName,
    login,
    logout,
    initializeAuth,
  }
})

Beachte die Struktur: Das ist die Setup Store Syntax (als Funktion, nicht als Options-Objekt) — sie spiegelt die Composition API wider und ist deutlich flexibler als die Options-Syntax. Claude Code wählt sie bewusst, weil du damit Composables direkt im Store nutzen kannst, TypeScript-Inferenz besser funktioniert und der Code lesbarer ist.

3. State, Getters und Actions — die richtige Verteilung

Eine der häufigsten Fehler beim Store-Design: zu viel in den Store packen. Alles landet im globalen State, weil es "vielleicht mal wo anders gebraucht wird". Das Ergebnis ist ein Store, der nach sechs Monaten niemand mehr überblickt.

Claude Code hilft, diese Grenze zu ziehen. Konkrete Frage:

claude "In meinem ProductStore habe ich currentPage, pageSize und searchQuery
als State. Ist das sinnvoll oder sollte das lokaler Component-State sein?"

Die Antwort hängt vom Kontext ab, den Claude Code in deiner Codebasis liest: Werden diese Werte von mehreren Komponenten gleichzeitig gelesen? Werden sie beim Routing persistiert? Müssen sie beim Seiten-Reload wiederhergestellt werden? Wenn nein — empfiehlt Claude Code, sie aus dem Store zu entfernen und lokal zu halten.

Getters: berechnete Werte sauber kapseln

Getters sind Pinia's Entsprechung von computed — und sie werden häufig untergenutzt. Statt berechneter Werte direkt in den Komponenten zu berechnen (was Duplikation erzeugt), gehören sie in den Store:

// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { CartItem } from '@/types/cart'

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const discountCode = ref<string | null>(null)
  const discountPercent = ref(0)

  // Getters — alle Berechnungen hier, nie in den Komponenten
  const itemCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const subtotal = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  const discountAmount = computed(() =>
    subtotal.value * (discountPercent.value / 100)
  )

  const total = computed(() => subtotal.value - discountAmount.value)

  const isEmpty = computed(() => items.value.length === 0)

  const hasDiscount = computed(() => discountPercent.value > 0)

  // Actions
  function addItem(item: CartItem) {
    const existing = items.value.find(i => i.id === item.id)
    if (existing) {
      existing.quantity += item.quantity
    } else {
      items.value.push({ ...item })
    }
  }

  function removeItem(itemId: string) {
    items.value = items.value.filter(i => i.id !== itemId)
  }

  function updateQuantity(itemId: string, quantity: number) {
    const item = items.value.find(i => i.id === itemId)
    if (item) {
      item.quantity = Math.max(0, quantity)
      if (item.quantity === 0) removeItem(itemId)
    }
  }

  async function applyDiscount(code: string) {
    // API-Call zur Validierung
    const result = await validateDiscountCode(code)
    if (result.valid) {
      discountCode.value = code
      discountPercent.value = result.percent
    }
    return result
  }

  function clearCart() {
    items.value = []
    discountCode.value = null
    discountPercent.value = 0
  }

  return {
    items,
    discountCode,
    itemCount,
    subtotal,
    discountAmount,
    total,
    isEmpty,
    hasDiscount,
    addItem,
    removeItem,
    updateQuantity,
    applyDiscount,
    clearCart,
  }
})

Claude Code generiert nicht nur den Store, sondern wählt auch die richtige Granularität der Getters — subtotal, discountAmount und total getrennt, weil jeder Wert einzeln in verschiedenen UI-Bereichen angezeigt wird.

4. Store-zu-Store-Kommunikation

In größeren Vue-Anwendungen hängen Stores voneinander ab — zum Beispiel braucht ein OrderStore Zugriff auf den AuthStore (für die User-ID) und den CartStore (für die Bestellpositionen). Das kann zu zirkulären Abhängigkeiten und Initialisierungsfehlern führen, wenn es nicht sauber gelöst wird.

claude "Mein OrderStore braucht Zugriff auf AuthStore und CartStore.
Wie strukturiere ich das sauber, ohne zirkuläre Abhängigkeiten zu riskieren?"

Claude Code liest alle drei Stores, identifiziert potenzielle Probleme und schreibt die Lösung:

// stores/order.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useAuthStore } from './auth'
import { useCartStore } from './cart'
import { createOrder } from '@/api/orders'
import type { Order } from '@/types/order'

export const useOrderStore = defineStore('order', () => {
  const orders = ref<Order[]>([])
  const currentOrder = ref<Order | null>(null)
  const loading = ref(false)

  // Stores innerhalb von Actions verwenden — nicht auf Store-Ebene
  // Das vermeidet Initialisierungsreihenfolge-Probleme
  async function placeOrder(shippingAddress: ShippingAddress) {
    const authStore = useAuthStore()
    const cartStore = useCartStore()

    if (!authStore.isAuthenticated) {
      throw new Error('Nicht eingeloggt')
    }

    if (cartStore.isEmpty) {
      throw new Error('Warenkorb ist leer')
    }

    loading.value = true
    try {
      const order = await createOrder({
        userId: authStore.user!.id,
        items: cartStore.items,
        total: cartStore.total,
        shippingAddress,
      })
      currentOrder.value = order
      orders.value.unshift(order)
      cartStore.clearCart()
      return order
    } finally {
      loading.value = false
    }
  }

  return { orders, currentOrder, loading, placeOrder }
})

Wichtig bei Store-zu-Store-Abhängigkeiten: Andere Stores immer innerhalb von Actions aufrufen, nicht auf der obersten Ebene der Setup-Funktion. Das verhindert Probleme mit der Initialisierungsreihenfolge, die besonders in Tests auftreten.

5. Pinia und Composables kombinieren

Eine der leistungsfähigsten Patterns in modernen Vue-Apps ist die Kombination von Pinia Stores mit Composables. Der Store hält den globalen State; Composables kapseln Logik, die diesen State nutzt und häufig gemeinsam benötigt wird.

claude "Erstelle ein Composable useProductSearch, das den ProductStore nutzt
und Suchlogik mit Debouncing und Loading-State kapselt."
// composables/useProductSearch.ts
import { ref, watch } from 'vue'
import { useProductStore } from '@/stores/product'
import { useDebounceFn } from '@vueuse/core'

export function useProductSearch() {
  const productStore = useProductStore()
  const searchQuery = ref('')
  const isSearching = ref(false)

  const performSearch = useDebounceFn(async (query: string) => {
    if (!query.trim()) {
      productStore.clearSearch()
      return
    }
    isSearching.value = true
    try {
      await productStore.searchProducts(query)
    } finally {
      isSearching.value = false
    }
  }, 300)

  watch(searchQuery, (newQuery) => {
    performSearch(newQuery)
  })

  function clearSearch() {
    searchQuery.value = ''
    productStore.clearSearch()
  }

  return {
    searchQuery,
    isSearching,
    results: productStore.searchResults,
    hasResults: productStore.hasSearchResults,
    clearSearch,
  }
}

Das Composable kapselt die Debounce-Logik, den Loading-State und den Watch — alles Dinge, die sonst in jeder Komponente, die eine Suche braucht, dupliziert würden. Der Store hält die eigentlichen Daten; das Composable hält die Interaktionslogik.

Das Composable in einer Komponente nutzen

<script setup lang="ts">
import { useProductSearch } from '@/composables/useProductSearch'

const { searchQuery, isSearching, results, hasResults, clearSearch }
  = useProductSearch()
</script>

<template>
  <div class="search-container">
    <input
      v-model="searchQuery"
      placeholder="Produkte suchen..."
      :disabled="isSearching"
    />
    <button v-if="searchQuery" @click="clearSearch">X</button>

    <div v-if="isSearching" class="loading">Suche...</div>

    <ul v-else-if="hasResults">
      <li v-for="product in results" :key="product.id">
        {{ product.name }} — {{ product.price }}€
      </li>
    </ul>

    <p v-else-if="searchQuery">Keine Ergebnisse für "{{ searchQuery }}"</p>
  </div>
</template>

Die Komponente ist minimal: keine eigene Logik, nur Darstellung. Die gesamte Intelligenz liegt im Composable und im Store.

6. Pinia Plugins für Persistenz und Logging

Pinia unterstützt Plugins — Funktionen, die auf jeden Store angewendet werden. Das häufigste Use-Case: State-Persistenz via localStorage oder sessionStorage.

claude "Erstelle ein Pinia Plugin, das bestimmte Stores im localStorage
persistiert, mit TypeScript-Support und optionaler Verschlüsselung
sensitiver Daten."
// plugins/persistedState.ts
import type { PiniaPluginContext } from 'pinia'

interface PersistOptions {
  key?: string
  paths?: string[]
  sensitive?: boolean
}

export function createPersistedState() {
  return ({ store, options }: PiniaPluginContext) => {
    const persist = (options as { persist?: PersistOptions }).persist
    if (!persist) return

    const storageKey = persist.key ?? store.$id
    const storage = persist.sensitive ? sessionStorage : localStorage

    // Beim Store-Start: gespeicherten State laden
    const stored = storage.getItem(storageKey)
    if (stored) {
      try {
        const parsed = JSON.parse(stored)
        // Nur konfigurierte Pfade wiederherstellen
        if (persist.paths) {
          persist.paths.forEach(path => {
            if (parsed[path] !== undefined) {
              store.$patch({ [path]: parsed[path] })
            }
          })
        } else {
          store.$patch(parsed)
        }
      } catch (e) {
        console.warn(`[pinia-persist] Fehler beim Laden von ${storageKey}`)
        storage.removeItem(storageKey)
      }
    }

    // Bei jedem State-Änderung: speichern
    store.$subscribe((_mutation, state) => {
      const toSave = persist.paths
        ? Object.fromEntries(
            persist.paths.map(path => [path, (state as Record<string, unknown>)[path]])
          )
        : state

      storage.setItem(storageKey, JSON.stringify(toSave))
    })
  }
}

// In main.ts registrieren:
// app.use(createPinia().use(createPersistedState()))

Und so wird ein Store damit annotiert:

// stores/userPreferences.ts
export const useUserPreferencesStore = defineStore(
  'userPreferences',
  () => {
    const theme = ref<'light' | 'dark'>('dark')
    const language = ref('de')
    const notificationsEnabled = ref(true)

    return { theme, language, notificationsEnabled }
  },
  {
    persist: {
      key: 'user-prefs',
      paths: ['theme', 'language'],
      // notificationsEnabled wird nicht persistiert
    },
  }
)

7. Stores testen mit Vitest

Pinia Stores sind eigenständige Einheiten, die sich gut isoliert testen lassen. Claude Code schreibt Tests, die den Store direkt instanziieren, ohne eine ganze Vue-App hochzufahren:

claude "Schreib Vitest-Tests für den AuthStore: Login-Erfolg,
Login-Fehler, Logout und den isAuthenticated-Getter."
// stores/__tests__/auth.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '../auth'
import * as authApi from '@/api/auth'

vi.mock('@/api/auth')

describe('useAuthStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.clearAllMocks()
  })

  it('startet mit unauthentifiziertem State', () => {
    const store = useAuthStore()
    expect(store.user).toBeNull()
    expect(store.isAuthenticated).toBe(false)
    expect(store.loading).toBe(false)
  })

  it('setzt user nach erfolgreichem Login', async () => {
    const mockUser = { id: '1', email: 'test@example.com', role: 'user' }
    vi.mocked(authApi.loginUser).mockResolvedValue({ user: mockUser })

    const store = useAuthStore()
    await store.login('test@example.com', 'password123')

    expect(store.user).toEqual(mockUser)
    expect(store.isAuthenticated).toBe(true)
    expect(store.loading).toBe(false)
    expect(store.error).toBeNull()
  })

  it('setzt error bei fehlgeschlagenem Login', async () => {
    vi.mocked(authApi.loginUser).mockRejectedValue(
      new Error('Ungültige Anmeldedaten')
    )

    const store = useAuthStore()
    await expect(store.login('wrong@example.com', 'wrong')).rejects.toThrow()

    expect(store.user).toBeNull()
    expect(store.isAuthenticated).toBe(false)
    expect(store.error).toBe('Ungültige Anmeldedaten')
  })

  it('räumt State bei Logout auf', async () => {
    const store = useAuthStore()
    store.user = { id: '1', email: 'test@example.com', role: 'user' }
    vi.mocked(authApi.logoutUser).mockResolvedValue(undefined)

    await store.logout()

    expect(store.user).toBeNull()
    expect(store.isAuthenticated).toBe(false)
  })

  it('isAdmin gibt true für admin-Rolle zurück', () => {
    const store = useAuthStore()
    store.user = { id: '1', email: 'admin@example.com', role: 'admin' }

    expect(store.isAdmin).toBe(true)
  })
})

Der entscheidende Punkt: setActivePinia(createPinia()) in beforeEach sorgt dafür, dass jeder Test eine frische Pinia-Instanz bekommt — kein State-Leck zwischen Tests. Claude Code setzt das automatisch, weil es die Pinia-Dokumentation kennt und weiß, dass das einer der häufigsten Testfehler ist.

8. Vue DevTools und Pinia: Debugging im Browser

Pinia ist tief in die Vue DevTools integriert. Jeder Store erscheint als eigener Eintrag, du siehst den aktuellen State, kannst ihn direkt ändern und siehst eine Timeline aller State-Mutationen. Claude Code kann dir helfen, diese Integration zu erweitern:

claude "Füge meinem ProductStore ein Custom-Label für die DevTools hinzu
und stelle sicher, dass die $reset-Funktion verfügbar ist."

In der Setup Store Syntax gibt es keine automatische $reset-Funktion (anders als in der Options-Syntax). Claude Code schreibt sie manuell:

// stores/product.ts
export const useProductStore = defineStore('product', () => {
  const initialState = () => ({
    products: [] as Product[],
    selectedProduct: null as Product | null,
    loading: false,
    error: null as string | null,
  })

  const state = reactive(initialState())

  // $reset manuell implementieren für Setup Stores
  function $reset() {
    Object.assign(state, initialState())
  }

  // ... weitere Actions und Getters

  return {
    ...toRefs(state),
    $reset,
    // weitere Exporte
  }
})

DevTools-Tipp: Die Vue DevTools zeigen für jeden Store den kompletten State-Baum, alle Getters und die Action-History. Wenn ein Getter nicht in den DevTools erscheint, hast du ihn wahrscheinlich vergessen zu exportieren — ein häufiger Fehler in der Setup-Syntax.

9. Migrierung von Vuex zu Pinia

Wer noch Vuex 4 einsetzt, hat in Claude Code ein starkes Migrations-Werkzeug. Der Prozess ist mechanisch genug, um automatisiert zu werden, aber komplex genug, dass man leicht Fehler macht — genau das richtige für Claude Code.

claude "Migriere dieses Vuex-Modul zu Pinia. Behalte alle Typen,
entferne den Mutations-Boilerplate und nutze die Setup-Syntax." < store/modules/cart.ts

Claude Code liest das Vuex-Modul mit seinen state-, mutations-, actions- und getters-Abschnitten und schreibt einen äquivalenten Pinia Store. Mutations verschwinden — in Pinia ändert man State direkt in Actions. Namespace-Patterns werden zu einfachen Store-IDs. Und das Ergebnis ist deutlich weniger Code.

Ein typischer Vuex-Store mit 150 Zeilen wird nach der Migration zu Pinia oft 80–90 Zeilen — ohne Funktionalitätsverlust.

10. Reale Zeitersparnis: Zahlen aus dem Produktiveinsatz

Was bringt Claude Code Pinia konkret in der täglichen Arbeit? Aus dem echten Einsatz:

Der größte Gewinn ist nicht Tipp-Geschwindigkeit, sondern Entscheidungsqualität. Claude Code sieht den gesamten Kontext und trägt Stores bei, die zur bestehenden Architektur passen — statt Insellösungen, die später refactort werden müssen.

"Claude Code spart nicht nur Zeit beim Schreiben — es spart vor allem die Zeit, die man später mit dem Aufräumen von schlecht strukturiertem State verbringt."

Zwei verwandte Artikel, die auf diesem Thema aufbauen:


Claude Code Mastery — von Pinia bis zum produktiven Agenten

Pinia und Vue sind ein Teil davon. Im Kurs lernst du Agents, MCP-Server, Hooks, Multi-Agent-Workflows und mehr — vollständig auf Deutsch, einmalig bezahlt.

Jetzt starten → Einmalzahlung · Kein Abo · 14 Tage Rückgaberecht

Kurs · Claude Code Mastery

Von Pinia zum produktiven AI-Agenten

Vue. Pinia. Agents. MCP. Hooks. Multi-Agent-Workflows. Alles auf Deutsch, einmalig bezahlt — kein Abo, keine Plattformabhängigkeit.

Jetzt einsteigen → Kursübersicht ansehen →

Einmalzahlung · Kein Abo · 14 Tage Rückgaberecht · Basis ab €29 · Pro ab €49