Vue.jsintermediateNew
Manage global app state in Vue 3 with Pinia (modern Vuex replacement)
✓Works with OpenClaudeYou are the #1 Vue 3 state management expert from Silicon Valley — the architect that companies use when their app outgrows component-local state. The user wants to set up Pinia for global state in their Vue 3 app.
What to check first
- Vue 3 + Pinia installed
- Identify what state should be global vs component-local
- Decide on store structure: by feature or by data type
Steps
- npm install pinia
- Create the Pinia instance in main.ts and use it
- Define stores with defineStore('storeId', { state, getters, actions })
- Use store in components via useUserStore()
- Reactive state should always go through state(), not as raw values
- For async operations, use actions
Code
// main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount('#app');
// stores/user.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
loading: false,
error: null as Error | null,
}),
getters: {
isAuthenticated: (state) => state.user !== null,
fullName: (state) => state.user ? `${state.user.firstName} ${state.user.lastName}` : '',
},
actions: {
async login(email: string, password: string) {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error('Login failed');
this.user = await response.json();
} catch (err) {
this.error = err as Error;
throw err;
} finally {
this.loading = false;
}
},
logout() {
this.user = null;
},
},
});
// stores/cart.ts — composition API style
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([]);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, item) => sum + item.qty, 0)
);
function add(item: CartItem) {
const existing = items.value.find((i) => i.id === item.id);
if (existing) {
existing.qty += item.qty;
} else {
items.value.push(item);
}
}
function remove(id: string) {
items.value = items.value.filter((i) => i.id !== id);
}
function clear() {
items.value = [];
}
return { items, total, itemCount, add, remove, clear };
});
// In a component
<script setup>
import { useUserStore } from '@/stores/user';
import { useCartStore } from '@/stores/cart';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const cartStore = useCartStore();
// Destructure with reactivity preserved
const { user, loading } = storeToRefs(userStore);
// Actions can be destructured directly (they're not reactive)
const { login, logout } = userStore;
</script>
<template>
<div v-if="user">{{ user.email }}</div>
<button @click="logout">Logout</button>
<div>Cart: {{ cartStore.itemCount }} items, ${{ cartStore.total }}</div>
</template>
Common Pitfalls
- Destructuring state directly without storeToRefs — loses reactivity
- Mutating arrays/objects directly inside getters
- Calling stores in script setup before app.use(pinia) — error
- Storing non-serializable data (Date objects, functions) — breaks devtools
When NOT to Use This Skill
- For component-local state — use ref() in setup
- For URL state — use vue-router instead
How to Verify It Worked
- Use Vue DevTools to inspect Pinia stores
- Test that state changes trigger component updates
Production Considerations
- Persist stores to localStorage with pinia-plugin-persistedstate
- Use TypeScript for type-safe state
- Don't put HTTP responses directly in state — transform first
Want a Vue.js skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.