
Pinia
- 185 installs
- 45 repo stars
- Updated December 6, 2025
- martinholovsky/claude-skills-generator
Implement Pinia stores, composables, and Vue 3 state patterns for reactive SaaS UIs, browser extensions, and content apps.
About
Pinia skill from martinholovsky/claude-skills-generator guides Claude through Vue 3 centralized state with Pinia—store design, typed actions and getters, composable integration, and maintainable patterns for scalable frontends.
- Store module structure and naming
- Composition API integration
- Actions, getters, and async flows
- Persistence and hydration patterns
- Testing and devtools-friendly stores
Pinia by the numbers
- 185 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #883 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill piniaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 185 |
|---|---|
| repo stars | ★ 45 |
| Last updated | December 6, 2025 |
| Repository | martinholovsky/claude-skills-generator ↗ |
What it does
Implement Pinia stores, composables, and Vue 3 state patterns for reactive SaaS UIs, browser extensions, and content apps.
Files
Pinia State Management Skill
File Organization: This skill uses split structure. See references/ for advanced patterns and security examples.1. Overview
This skill provides Pinia expertise for managing application state in the JARVIS AI Assistant, including system metrics, user preferences, and HUD configuration.
Risk Level: MEDIUM - Manages sensitive state, SSR considerations, potential data exposure
Primary Use Cases:
- System metrics and status tracking
- User preferences and settings
- HUD configuration state
- Command history and queue
- Real-time data synchronization
2. Core Responsibilities
2.1 Core Principles
1. TDD First: Write store tests before implementation 2. Performance Aware: Optimize subscriptions and computed values 3. Type Safety: Define stores with full TypeScript typing 4. SSR Security: Prevent state leakage between requests 5. Composition API: Use setup stores for better TypeScript support 6. Minimal State: Store only necessary data, derive the rest 7. Action Validation: Validate inputs in actions before mutations 8. Persistence Security: Never persist sensitive data to localStorage
3. Technology Stack & Versions
3.1 Recommended Versions
| Package | Version | Notes |
|---|---|---|
| pinia | ^2.1.0 | Latest stable |
| @pinia/nuxt | ^0.5.0 | Nuxt integration |
| pinia-plugin-persistedstate | ^3.0.0 | Optional persistence |
3.2 Nuxt Configuration
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
storesDirs: ['./stores/**']
}
})3.3 Implementation Workflow (TDD)
Follow this workflow for every store:
Step 1: Write Failing Test First
// tests/stores/metrics.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useMetricsStore } from '~/stores/metrics'
describe('MetricsStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with default values', () => {
const store = useMetricsStore()
expect(store.cpu).toBe(0)
expect(store.memory).toBe(0)
})
it('should clamp values within valid range', () => {
const store = useMetricsStore()
store.updateCpu(150)
expect(store.cpu).toBe(100)
store.updateCpu(-50)
expect(store.cpu).toBe(0)
})
it('should compute health status correctly', () => {
const store = useMetricsStore()
store.updateCpu(95)
store.updateMemory(90)
expect(store.healthStatus).toBe('critical')
})
})Step 2: Implement Minimum to Pass
// stores/metrics.ts
export const useMetricsStore = defineStore('metrics', () => {
const cpu = ref(0)
const memory = ref(0)
const healthStatus = computed(() => {
const avg = (cpu.value + memory.value) / 2
if (avg > 90) return 'critical'
if (avg > 70) return 'warning'
return 'healthy'
})
function updateCpu(value: number) {
cpu.value = Math.max(0, Math.min(100, value))
}
function updateMemory(value: number) {
memory.value = Math.max(0, Math.min(100, value))
}
return { cpu, memory, healthStatus, updateCpu, updateMemory }
})Step 3: Refactor Following Patterns
- Extract validation logic
- Add TypeScript interfaces
- Optimize computed dependencies
Step 4: Run Full Verification
npm run test -- --filter=stores
npm run typecheck
npm run build4. Implementation Patterns
4.1 Setup Store with TypeScript
// stores/jarvis.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface SystemMetrics {
cpu: number
memory: number
network: number
timestamp: number
}
interface JARVISState {
status: 'idle' | 'listening' | 'processing' | 'responding'
securityLevel: 'normal' | 'elevated' | 'lockdown'
}
export const useJarvisStore = defineStore('jarvis', () => {
// State
const state = ref<JARVISState>({
status: 'idle',
securityLevel: 'normal'
})
const metrics = ref<SystemMetrics>({
cpu: 0,
memory: 0,
network: 0,
timestamp: Date.now()
})
// Getters
const isActive = computed(() =>
state.value.status !== 'idle'
)
const systemHealth = computed(() => {
const avg = (metrics.value.cpu + metrics.value.memory) / 2
if (avg > 90) return 'critical'
if (avg > 70) return 'warning'
return 'healthy'
})
// Actions
function updateMetrics(newMetrics: Partial<SystemMetrics>) {
// ✅ Validate input
if (newMetrics.cpu !== undefined) {
metrics.value.cpu = Math.max(0, Math.min(100, newMetrics.cpu))
}
if (newMetrics.memory !== undefined) {
metrics.value.memory = Math.max(0, Math.min(100, newMetrics.memory))
}
if (newMetrics.network !== undefined) {
metrics.value.network = Math.max(0, newMetrics.network)
}
metrics.value.timestamp = Date.now()
}
function setStatus(newStatus: JARVISState['status']) {
state.value.status = newStatus
}
function setSecurityLevel(level: JARVISState['securityLevel']) {
state.value.securityLevel = level
// ✅ Audit security changes
console.info(`Security level changed to: ${level}`)
}
return {
state,
metrics,
isActive,
systemHealth,
updateMetrics,
setStatus,
setSecurityLevel
}
})4.2 User Preferences Store (with Persistence)
// stores/preferences.ts
export const usePreferencesStore = defineStore('preferences', () => {
const preferences = ref({
theme: 'dark' as 'dark' | 'light',
hudOpacity: 0.8,
soundEnabled: true
})
function updatePreference<K extends keyof typeof preferences.value>(
key: K, value: typeof preferences.value[K]
) {
if (key === 'hudOpacity' && (value < 0 || value > 1)) return
preferences.value[key] = value
}
return { preferences, updatePreference }
}, {
persist: {
key: 'jarvis-preferences',
paths: ['preferences.theme', 'preferences.hudOpacity']
// ❌ Never persist: tokens, passwords, API keys
}
})4.3 Command Queue Store
// stores/commands.ts
interface Command {
id: string
action: string
status: 'pending' | 'executing' | 'completed' | 'failed'
}
export const useCommandStore = defineStore('commands', () => {
const queue = ref<Command[]>([])
const history = ref<Command[]>([])
const MAX_HISTORY = 100
const pendingCommands = computed(() =>
queue.value.filter(cmd => cmd.status === 'pending')
)
function addCommand(action: string) {
const cmd: Command = { id: crypto.randomUUID(), action, status: 'pending' }
queue.value.push(cmd)
return cmd.id
}
function completeCommand(id: string, status: 'completed' | 'failed') {
const idx = queue.value.findIndex(cmd => cmd.id === id)
if (idx !== -1) {
const [cmd] = queue.value.splice(idx, 1)
cmd.status = status
history.value = [cmd, ...history.value].slice(0, MAX_HISTORY)
}
}
return { queue, history, pendingCommands, addCommand, completeCommand }
})4.4 SSR-Safe Store Usage
<script setup lang="ts">
// ✅ Safe for SSR - store initialized per-request
const jarvisStore = useJarvisStore()
// ✅ Fetch data on server
const { data } = await useFetch('/api/metrics')
// Update store with fetched data
if (data.value) {
jarvisStore.updateMetrics(data.value)
}
</script>4.5 Store Composition
// stores/dashboard.ts
export const useDashboardStore = defineStore('dashboard', () => {
// ✅ Compose from other stores
const jarvisStore = useJarvisStore()
const commandStore = useCommandStore()
const dashboardStatus = computed(() => ({
systemHealth: jarvisStore.systemHealth,
pendingCommands: commandStore.pendingCommands.length,
isActive: jarvisStore.isActive
}))
return {
dashboardStatus
}
})5. Security Standards
5.1 OWASP Coverage
| OWASP Category | Risk | Mitigation |
|---|---|---|
| A01 Broken Access Control | MEDIUM | Validate actions, check permissions |
| A04 Insecure Design | MEDIUM | SSR state isolation |
| A07 Auth Failures | MEDIUM | Never persist tokens |
5.3 Sensitive Data Handling
// ❌ NEVER persist: tokens, API keys, passwords
// ✅ Store sensitive data in memory only (no persist option)
const authStore = defineStore('auth', () => {
const token = ref<string | null>(null)
return { token }
})5.5 Performance Patterns
Pattern 1: Selective Subscriptions
// BAD - Subscribes to entire store
const store = useJarvisStore()
watch(() => store.state, () => { /* ... */ }, { deep: true })
// GOOD - Subscribe to specific properties
const store = useJarvisStore()
watch(() => store.state.status, (newStatus) => {
console.log('Status changed:', newStatus)
})Pattern 2: Computed Getters (Memoization)
// BAD - Recalculates on every access
function getFilteredItems() {
return items.value.filter(i => i.active)
}
// GOOD - Cached until dependencies change
const filteredItems = computed(() =>
items.value.filter(i => i.active)
)Pattern 3: Batch Updates
// BAD - Multiple reactive triggers
function updateAll(data: MetricsData) {
metrics.value.cpu = data.cpu
metrics.value.memory = data.memory
metrics.value.network = data.network
}
// GOOD - Single reactive trigger
function updateAll(data: MetricsData) {
metrics.value = { ...metrics.value, ...data, timestamp: Date.now() }
}Pattern 4: Lazy Store Initialization
// BAD - Store initializes immediately
const heavyStore = useHeavyDataStore()
// GOOD - Initialize only when needed
const heavyStore = ref<ReturnType<typeof useHeavyDataStore> | null>(null)
function loadHeavyData() {
if (!heavyStore.value) {
heavyStore.value = useHeavyDataStore()
}
return heavyStore.value
}Pattern 5: Optimistic Updates
// BAD - Wait for server response
async function deleteItem(id: string) {
await api.delete(`/items/${id}`)
items.value = items.value.filter(i => i.id !== id)
}
// GOOD - Update immediately, rollback on error
async function deleteItem(id: string) {
const backup = [...items.value]
items.value = items.value.filter(i => i.id !== id)
try {
await api.delete(`/items/${id}`)
} catch (error) {
items.value = backup // Rollback
throw error
}
}6. Testing & Quality
See Section 3.3 for complete TDD workflow with vitest examples.
8. Common Anti-Patterns
Security Anti-Patterns
// ❌ Global state leaks between SSR users
const state = reactive({ user: null })
// ✅ Pinia isolates per-request
export const useUserStore = defineStore('user', () => {
const user = ref(null)
return { user }
})
// ❌ Never persist auth tokens (XSS risk)
persist: { paths: ['authToken'] }
// ✅ Use httpOnly cookies for authPerformance Anti-Patterns
See Section 5.5 for detailed performance patterns with Good/Bad examples.
13. Pre-Implementation Checklist
Phase 1: Before Writing Code
- [ ] Store interface designed with TypeScript types
- [ ] Test file created with failing tests
- [ ] Security requirements identified (persistence, SSR)
- [ ] Performance patterns selected for use case
Phase 2: During Implementation
- [ ] Tests passing after each feature added
- [ ] Actions validate all inputs
- [ ] Computed values use minimal dependencies
- [ ] No sensitive data in persisted state
- [ ] SSR state properly isolated
Phase 3: Before Committing
- [ ] All store tests passing:
npm run test -- --filter=stores - [ ] Type check passing:
npm run typecheck - [ ] Build succeeds:
npm run build - [ ] No global state outside Pinia
- [ ] State shape documented in types
14. Summary
Pinia provides type-safe state management for JARVIS:
1. TDD First: Write store tests before implementation 2. Performance: Optimize subscriptions and computed values 3. Security: Never persist sensitive data, isolate SSR state 4. Type Safety: Use setup stores with full TypeScript
References: See references/ for advanced patterns and security examples.
Pinia Advanced Patterns
Plugin Development
Persistence Plugin
// plugins/piniaLogger.ts
import { PiniaPluginContext } from 'pinia'
export function piniaLogger({ store }: PiniaPluginContext) {
store.$subscribe((mutation, state) => {
console.log(`[${store.$id}] ${mutation.type}`, {
storeId: mutation.storeId,
payload: mutation.payload
})
})
}
// Usage
const pinia = createPinia()
pinia.use(piniaLogger)State Hydration for SSR
// plugins/piniaHydration.ts
export function hydrateStores(pinia: Pinia) {
if (import.meta.server) return
// Hydrate from SSR payload
const nuxtApp = useNuxtApp()
if (nuxtApp.payload.pinia) {
pinia.state.value = nuxtApp.payload.pinia
}
}Optimistic Updates
export const useItemsStore = defineStore('items', () => {
const items = ref<Item[]>([])
async function deleteItem(id: string) {
// Save original for rollback
const original = [...items.value]
const index = items.value.findIndex(i => i.id === id)
// Optimistic update
items.value.splice(index, 1)
try {
await $fetch(`/api/items/${id}`, { method: 'DELETE' })
} catch (error) {
// Rollback on error
items.value = original
throw error
}
}
return { items, deleteItem }
})Computed with Parameters
export const useFilterStore = defineStore('filter', () => {
const items = ref<Item[]>([])
// Getter that returns a function
const getByCategory = computed(() => {
return (category: string) =>
items.value.filter(item => item.category === category)
})
// Usage: store.getByCategory('electronics')
return { items, getByCategory }
})Real-Time Sync
export const useRealtimeStore = defineStore('realtime', () => {
const data = ref<RealtimeData | null>(null)
let socket: WebSocket | null = null
function connect() {
socket = new WebSocket('wss://api.example.com/ws')
socket.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.type === 'update') {
data.value = message.data
}
}
}
function disconnect() {
socket?.close()
socket = null
}
return { data, connect, disconnect }
})Pinia Security Examples
SSR State Isolation
Preventing State Leakage
// ❌ DANGEROUS - Shared across all requests
// store.ts
let globalData = null
export function getData() {
return globalData
}
// ✅ SECURE - Per-request isolation
export const useDataStore = defineStore('data', () => {
// Each request gets fresh state
const data = ref(null)
return { data }
})Nuxt Server-Side Security
// server/api/user.ts
export default defineEventHandler(async (event) => {
// Get user from session, not from client state
const session = await getSession(event)
if (!session.user) {
throw createError({ statusCode: 401 })
}
// Never trust state from client
return { user: session.user }
})Sensitive Data Protection
Memory-Only Storage
export const useAuthStore = defineStore('auth', () => {
// Sensitive data - memory only
const accessToken = ref<string | null>(null)
const refreshToken = ref<string | null>(null)
// Non-sensitive - can persist
const userPreferences = ref({})
return {
accessToken,
refreshToken,
userPreferences
}
// NO persist option for sensitive data
})Secure Token Handling
export const useAuthStore = defineStore('auth', () => {
const accessToken = ref<string | null>(null)
async function login(credentials: Credentials) {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials
})
// Token in memory only
accessToken.value = response.accessToken
// Set httpOnly cookie on server
// (handled by server response)
}
function logout() {
// Clear memory
accessToken.value = null
// Clear httpOnly cookie
$fetch('/api/auth/logout', { method: 'POST' })
}
return { accessToken, login, logout }
})Action Validation
Input Sanitization
import { z } from 'zod'
const userUpdateSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
bio: z.string().max(500).optional()
})
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
async function updateUser(updates: unknown) {
// Validate before mutation
const validated = userUpdateSchema.parse(updates)
// Safe to update
const response = await $fetch('/api/user', {
method: 'PATCH',
body: validated
})
user.value = response
}
return { user, updateUser }
})Permission Checking
export const useAdminStore = defineStore('admin', () => {
const currentUser = computed(() => useAuthStore().user)
async function deleteUser(userId: string) {
// Check permission before action
if (!currentUser.value?.permissions.includes('admin')) {
throw new Error('Unauthorized')
}
await $fetch(`/api/admin/users/${userId}`, {
method: 'DELETE'
})
}
return { deleteUser }
})