Agent skill
pinia
Manages Vue state with Pinia including stores, getters, actions, and plugins. Use when building Vue applications needing centralized state, sharing state between components, or replacing Vuex.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/pinia-mgd34msu-goodvibes-gemini-dafd3073
SKILL.md
Pinia
The intuitive, type safe, and flexible store for Vue.
Quick Start
Install:
npm install pinia
Setup (main.ts):
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
Defining Stores
Option Store (Vue Options API style)
// stores/counter.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter',
}),
getters: {
doubleCount: (state) => state.count * 2,
// Getter using other getters
doubleCountPlusOne(): number {
return this.doubleCount + 1;
},
},
actions: {
increment() {
this.count++;
},
async fetchAndSet() {
const response = await fetch('/api/count');
const data = await response.json();
this.count = data.count;
},
},
});
Setup Store (Composition API style)
// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0);
const name = ref('Counter');
// Getters
const doubleCount = computed(() => count.value * 2);
// Actions
function increment() {
count.value++;
}
async function fetchAndSet() {
const response = await fetch('/api/count');
const data = await response.json();
count.value = data.count;
}
return { count, name, doubleCount, increment, fetchAndSet };
});
Using Stores
Basic Usage
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
// Access state
console.log(counter.count);
// Access getters
console.log(counter.doubleCount);
// Call actions
counter.increment();
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
</div>
</template>
Destructuring with storeToRefs
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
// Destructure with reactivity preserved
const { count, doubleCount } = storeToRefs(counter);
// Actions can be destructured directly
const { increment } = counter;
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
State
Accessing State
const store = useCounterStore();
// Direct access
store.count;
// Via $state
store.$state.count;
Modifying State
const store = useCounterStore();
// Direct mutation
store.count++;
// Patch single property
store.$patch({ count: 10 });
// Patch multiple properties
store.$patch({
count: 10,
name: 'New Counter',
});
// Patch with function
store.$patch((state) => {
state.count++;
state.items.push({ id: 1 });
});
// Replace entire state
store.$state = { count: 0, name: 'Reset' };
// Reset to initial state
store.$reset();
Getters
Basic Getters
export const useProductStore = defineStore('products', {
state: () => ({
items: [] as Product[],
}),
getters: {
// Arrow function
itemCount: (state) => state.items.length,
// Using this for other getters
hasItems(): boolean {
return this.itemCount > 0;
},
// Getter with parameter (returns function)
getById: (state) => {
return (id: string) => state.items.find(item => item.id === id);
},
},
});
Using Other Store Getters
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', {
getters: {
userCart(): CartItem[] {
const userStore = useUserStore();
return this.items.filter(item => item.userId === userStore.currentUserId);
},
},
});
Actions
Basic Actions
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
token: null as string | null,
}),
actions: {
async login(email: string, password: string) {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
this.user = data.user;
this.token = data.token;
return data;
} catch (error) {
this.user = null;
this.token = null;
throw error;
}
},
logout() {
this.user = null;
this.token = null;
this.$reset();
},
},
});
Using Other Stores in Actions
import { useNotificationStore } from './notification';
export const useCartStore = defineStore('cart', {
actions: {
async checkout() {
const notificationStore = useNotificationStore();
try {
await this.submitOrder();
notificationStore.show('Order placed!');
} catch (error) {
notificationStore.show('Order failed', 'error');
}
},
},
});
Subscribing to Changes
State Subscription
const store = useCounterStore();
// Subscribe to state changes
store.$subscribe((mutation, state) => {
console.log('State changed:', mutation.type);
console.log('New state:', state);
// Persist to localStorage
localStorage.setItem('counter', JSON.stringify(state));
});
// With options
store.$subscribe(
(mutation, state) => {
// ...
},
{ detached: true } // Survives component unmount
);
Action Subscription
const store = useAuthStore();
// Subscribe to actions
store.$onAction(({ name, args, after, onError }) => {
console.log(`Action ${name} called with:`, args);
after((result) => {
console.log(`Action ${name} finished with:`, result);
});
onError((error) => {
console.error(`Action ${name} failed:`, error);
});
});
Plugins
Creating a Plugin
// plugins/persistedState.ts
import { PiniaPluginContext } from 'pinia';
export function piniaPersistedState({ store }: PiniaPluginContext) {
// Restore state from localStorage
const savedState = localStorage.getItem(store.$id);
if (savedState) {
store.$patch(JSON.parse(savedState));
}
// Subscribe to changes
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}
// main.ts
const pinia = createPinia();
pinia.use(piniaPersistedState);
Adding Properties to Stores
import { markRaw } from 'vue';
import { Router } from 'vue-router';
declare module 'pinia' {
export interface PiniaCustomProperties {
router: Router;
}
}
const pinia = createPinia();
pinia.use(({ store }) => {
store.router = markRaw(router);
});
TypeScript
Typed Store
interface UserState {
user: User | null;
isLoading: boolean;
error: string | null;
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
user: null,
isLoading: false,
error: null,
}),
getters: {
isLoggedIn: (state): boolean => !!state.user,
fullName(): string {
return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';
},
},
actions: {
async fetchUser(id: string): Promise<void> {
this.isLoading = true;
try {
const response = await fetch(`/api/users/${id}`);
this.user = await response.json();
} catch (e) {
this.error = (e as Error).message;
} finally {
this.isLoading = false;
}
},
},
});
Testing
import { setActivePinia, createPinia } from 'pinia';
import { useCounterStore } from '@/stores/counter';
import { describe, it, expect, beforeEach } from 'vitest';
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('increments count', () => {
const counter = useCounterStore();
expect(counter.count).toBe(0);
counter.increment();
expect(counter.count).toBe(1);
});
it('computes double count', () => {
const counter = useCounterStore();
counter.count = 5;
expect(counter.doubleCount).toBe(10);
});
});
Composing Stores
// stores/cart.ts
import { useProductStore } from './products';
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', () => {
const productStore = useProductStore();
const userStore = useUserStore();
const items = ref<CartItem[]>([]);
const total = computed(() => {
return items.value.reduce((sum, item) => {
const product = productStore.getById(item.productId);
return sum + (product?.price ?? 0) * item.quantity;
}, 0);
});
const discountedTotal = computed(() => {
const discount = userStore.user?.discount ?? 0;
return total.value * (1 - discount);
});
return { items, total, discountedTotal };
});
Best Practices
- One store per domain - User store, cart store, etc.
- Use Setup Stores for complex logic - Better composition
- Use storeToRefs for destructuring - Maintains reactivity
- Keep actions async-aware - Return promises
- Use plugins for cross-cutting concerns - Persistence, logging
Common Mistakes
| Mistake | Fix |
|---|---|
| Destructuring state directly | Use storeToRefs() |
| Calling useStore outside setup | Call inside setup or actions |
| Mutating state in getters | Keep getters pure |
| Circular store dependencies | Refactor to avoid cycles |
| Not using $reset | Use it to reset to initial |
Reference Files
- references/plugins.md - Plugin patterns
- references/testing.md - Testing stores
- references/composition.md - Store composition
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?