Claude Code for Pinia: The Intuitive Vue Store — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Pinia: The Intuitive Vue Store
Frontend

Claude Code for Pinia: The Intuitive Vue Store

Published: May 8, 2027
Read time: 6 min read
By: Claude Skills 360

Pinia is the official Vue 3 state management library — defineStore("id", () => { ... }) uses the setup function syntax returning state refs and actions. const { count } = storeToRefs(useCounterStore()) destructures with preserved reactivity. store.$patch({ key: value }) batches multiple state updates atomically. store.$reset() restores initial state. store.$subscribe((mutation, state) => ...) watches state changes. store.$onAction(({ name, after, onError }) => ...) hooks into action calls. Cross-store composition works by calling useOtherStore() inside the setup function. Pinia plugins add properties via pinia.use(plugin)pinia-plugin-persistedstate persists state to localStorage. defineStore("id", { state, getters, actions }) is the options API alternative. Vue DevTools shows the Pinia panel for time-travel debugging. SSR hydration uses initialState for server-rendered state. Claude Code generates Pinia cart stores, user auth stores, filter stores, optimistic UI patterns, and persistent preference stores.

CLAUDE.md for Pinia

## Pinia Stack
- Version: pinia >= 2.2, pinia-plugin-persistedstate >= 3.2
- Setup: const useCartStore = defineStore("cart", () => { const items = ref<CartItem[]>([]); ... return { items, ... } })
- Access: const store = useCartStore(); const { items } = storeToRefs(store) — preserves reactivity
- Patch: store.$patch({ loading: false, items: [] }) — batch update
- Reset: store.$reset() — options API only; setup stores need manual reset action
- Persist: { persist: { key: "cart", storage: localStorage } } — via pinia-plugin-persistedstate
- Plugin: app.use(createPinia().use(piniaPluginPersistedstate))

Cart Store

// stores/cart.ts — shopping cart with Pinia setup syntax
import { defineStore } from "pinia"
import { ref, computed } from "vue"

export type CartItem = {
  id: string
  productId: string
  name: string
  price: number  // cents
  quantity: number
  imageUrl: string
  variantId?: string
}

export const useCartStore = defineStore(
  "cart",
  () => {
    // State
    const items = ref<CartItem[]>([])
    const couponCode = ref<string | null>(null)
    const isOpen = ref(false)

    // Getters
    const itemCount = computed(() =>
      items.value.reduce((sum, item) => sum + item.quantity, 0),
    )

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

    const discountCents = computed(() => {
      if (couponCode.value === "SAVE10") return Math.round(subtotalCents.value * 0.1)
      return 0
    })

    const totalCents = computed(() =>
      Math.max(0, subtotalCents.value - discountCents.value),
    )

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

    // Actions
    function addItem(newItem: Omit<CartItem, "quantity">, quantity = 1) {
      const existing = items.value.find(
        i => i.productId === newItem.productId && i.variantId === newItem.variantId,
      )

      if (existing) {
        existing.quantity += quantity
      } else {
        items.value.push({ ...newItem, quantity })
      }
    }

    function removeItem(itemId: string) {
      const idx = items.value.findIndex(i => i.id === itemId)
      if (idx !== -1) items.value.splice(idx, 1)
    }

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

    function applyCoupon(code: string) {
      const valid = ["SAVE10", "WELCOME20"]
      if (!valid.includes(code.toUpperCase())) {
        throw new Error("Invalid coupon code")
      }
      couponCode.value = code.toUpperCase()
    }

    function clearCoupon() {
      couponCode.value = null
    }

    function clear() {
      items.value = []
      couponCode.value = null
    }

    function toggle() {
      isOpen.value = !isOpen.value
    }

    return {
      // State
      items,
      couponCode,
      isOpen,
      // Getters
      itemCount,
      subtotalCents,
      discountCents,
      totalCents,
      isEmpty,
      // Actions
      addItem,
      removeItem,
      updateQuantity,
      applyCoupon,
      clearCoupon,
      clear,
      toggle,
    }
  },
  {
    // Persist via pinia-plugin-persistedstate
    persist: {
      key: "cart-v2",
      storage: typeof window !== "undefined" ? localStorage : undefined,
      pick: ["items", "couponCode"],  // Don't persist UI state
    },
  },
)

Auth Store with Cross-Store Dependency

// stores/auth.ts — user auth + cross-store cart merge
import { defineStore } from "pinia"
import { ref, computed } from "vue"
import { useCartStore } from "./cart"

export type User = {
  id: string
  name: string
  email: string
  role: "user" | "admin"
  avatarUrl: string | null
}

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

  const isAuthenticated = computed(() => user.value !== null)
  const isAdmin = computed(() => user.value?.role === "admin")

  async function login(email: string, password: string) {
    loading.value = true
    error.value = null

    try {
      const res = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      })

      if (!res.ok) {
        const data = await res.json()
        throw new Error(data.error ?? "Login failed")
      }

      const { user: userData } = await res.json()
      user.value = userData

      // Merge guest cart into user cart
      const cartStore = useCartStore()
      if (!cartStore.isEmpty) {
        await fetch("/api/cart/merge", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ items: cartStore.items }),
        })
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : "Unknown error"
      throw err
    } finally {
      loading.value = false
    }
  }

  async function logout() {
    await fetch("/api/auth/logout", { method: "POST" })
    user.value = null

    const cartStore = useCartStore()
    cartStore.clear()
  }

  async function fetchCurrentUser() {
    loading.value = true
    try {
      const res = await fetch("/api/auth/me")
      if (res.ok) {
        user.value = await res.json()
      }
    } finally {
      loading.value = false
    }
  }

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

Vue Component Usage

<!-- components/CartDrawer.vue — cart UI with Pinia -->
<script setup lang="ts">
import { storeToRefs } from "pinia"
import { useCartStore } from "@/stores/cart"

const cart = useCartStore()
// storeToRefs preserves reactivity when destructuring
const { items, itemCount, subtotalCents, totalCents, discountCents, isOpen } = storeToRefs(cart)

function formatCents(cents: number) {
  return (cents / 100).toLocaleString("en-US", { style: "currency", currency: "USD" })
}
</script>

<template>
  <Transition name="slide">
    <div v-if="isOpen" class="fixed right-0 top-0 h-full w-96 bg-background border-l shadow-xl z-50 flex flex-col">
      <!-- Header -->
      <div class="flex items-center justify-between p-4 border-b">
        <h2 class="font-semibold text-lg">Cart ({{ itemCount }})</h2>
        <button @click="cart.toggle()" class="p-1 rounded hover:bg-muted">✕</button>
      </div>

      <!-- Items -->
      <div class="flex-1 overflow-y-auto p-4 space-y-3">
        <div v-if="cart.isEmpty" class="text-center text-muted-foreground py-12">
          Your cart is empty
        </div>

        <div v-for="item in items" :key="item.id" class="flex gap-3 p-3 rounded-xl border">
          <img :src="item.imageUrl" :alt="item.name" class="size-16 object-cover rounded-lg" />
          <div class="flex-1 min-w-0">
            <p class="font-medium text-sm truncate">{{ item.name }}</p>
            <p class="text-muted-foreground text-sm">{{ formatCents(item.price) }}</p>
            <div class="flex items-center gap-2 mt-1">
              <button @click="cart.updateQuantity(item.id, item.quantity - 1)" class="size-6 rounded border text-sm">-</button>
              <span class="text-sm w-6 text-center">{{ item.quantity }}</span>
              <button @click="cart.updateQuantity(item.id, item.quantity + 1)" class="size-6 rounded border text-sm">+</button>
            </div>
          </div>
          <button @click="cart.removeItem(item.id)" class="text-muted-foreground hover:text-destructive text-sm">✕</button>
        </div>
      </div>

      <!-- Footer -->
      <div v-if="!cart.isEmpty" class="p-4 border-t space-y-3">
        <div v-if="discountCents > 0" class="flex justify-between text-sm text-green-600">
          <span>Discount</span>
          <span>-{{ formatCents(discountCents) }}</span>
        </div>
        <div class="flex justify-between font-semibold">
          <span>Total</span>
          <span>{{ formatCents(totalCents) }}</span>
        </div>
        <a href="/checkout" class="btn-primary w-full text-center block">
          Checkout
        </a>
      </div>
    </div>
  </Transition>
</template>

For the Vuex alternative when a large Vue 2 codebase with existing Vuex modules needs state management — Pinia is the official successor to Vuex and should be used for all new Vue 3 projects, while Vuex 4 is maintained only for Vue 3 migration compatibility, see the Vuex guide. For the Zustand with React alternative when building the same feature in a React application — Pinia is Vue-ecosystem only while Zustand fills the same role in React with a similar lightweight, non-opinionated API, see the Zustand guide. The Claude Skills 360 bundle includes Pinia skill sets covering store design, cross-store composition, and persistence. Start with the free tier to try Vue state management generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free