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.
Creates and manages Pinia stores for Vue apps with state, getters, actions, and plugins. Use when building Vue applications that need centralized state management or shared state between components.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
The intuitive, type safe, and flexible store for Vue.
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');
// 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;
},
},
});
// 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 };
});
<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>
<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>
const store = useCounterStore();
// Direct access
store.count;
// Via $state
store.$state.count;
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();
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);
},
},
});
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', {
getters: {
userCart(): CartItem[] {
const userStore = useUserStore();
return this.items.filter(item => item.userId === userStore.currentUserId);
},
},
});
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();
},
},
});
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');
}
},
},
});
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
);
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/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);
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);
});
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;
}
},
},
});
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);
});
});
// 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 };
});
| 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 |
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.