Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
- Vue 3 fundamentals 🟢
What you'll learn
- Understand Pinia store fundamentals 🎯
- Apply Pinia in real Vue projects 🏗️
- Debug common state management issues 🐛
- Write type-safe state management code ✨
🎯 Introduction
Welcome to the exciting world of Pinia! 🎉 Say goodbye to complex state management and hello to the modern, intuitive way to handle your Vue 3 applications’ state.
Think of Pinia as your app’s smart assistant 🤖 - it keeps track of everything important, shares information between components, and makes sure your data stays organized and accessible. Whether you’re building a shopping app 🛒, a social media platform 📱, or a dashboard 📊, Pinia will make your state management journey smooth and enjoyable!
By the end of this tutorial, you’ll be creating stores like a pro and managing state with confidence! Let’s dive into the pineapple goodness! 🍍
📚 Understanding Pinia
🤔 What is Pinia?
Pinia is like having a super-organized friend 🧠 who remembers everything for you! Think of it as a central warehouse 🏬 where all your app’s important data lives, perfectly organized and easily accessible from any part of your application.
In Vue terms, Pinia is the official state management library that provides:
- ✨ Intuitive API - Simple and straightforward
- 🚀 Great Performance - Lightweight and fast
- 🛡️ Type Safety - Full TypeScript support
- 🔧 DevTools Support - Excellent debugging experience
💡 Why Use Pinia Over Vuex?
Here’s why developers are switching to Pinia:
- Simpler API 🎯: No mutations, actions are just functions
- Better TypeScript 💙: Native TypeScript support without extra setup
- Modular Design 🧩: Each store is independent and focused
- Hot Module Replacement 🔥: Better development experience
- Smaller Bundle 📦: Less code means faster apps
Real-world example: Imagine building a todo app 📝. With Pinia, your todo store is just a simple object with state, getters, and actions - no confusing boilerplate!
🔧 Basic Syntax and Usage
📦 Installation and Setup
Let’s get Pinia ready in your Vue project:
# 🍍 Install Pinia
npm install pinia
// 🏗️ main.ts - Setting up Pinia
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia(); // 🍍 Create our pineapple store factory
app.use(pinia); // 🎯 Tell Vue to use Pinia
app.mount('#app');
🏪 Creating Your First Store
Here’s the magic of Pinia stores:
// 🍍 stores/counter.ts - Simple counter store
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
// 📊 State - your data lives here
state: () => ({
count: 0,
name: 'Awesome Counter! 🚀'
}),
// 📖 Getters - computed values
getters: {
// 🎯 Double the count
doubleCount: (state) => state.count * 2,
// 🎨 Formatted display
displayText: (state) => `${state.name}: ${state.count} 🎉`
},
// 🎬 Actions - methods that can modify state
actions: {
// ➕ Increment counter
increment() {
this.count++; // 🚀 Direct mutation - so simple!
console.log(`📈 Count increased to ${this.count}!`);
},
// 🎯 Set to specific value
setCount(value: number) {
this.count = value;
console.log(`🎯 Count set to ${value}!`);
},
// 🔄 Reset to zero
reset() {
this.count = 0;
console.log('🔄 Counter reset! Starting fresh! ✨');
}
}
});
💡 Pro Tip: Notice how clean this is! No mutations, no commit() calls - just simple, direct state updates!
💡 Practical Examples
🛒 Example 1: Shopping Cart Store
Let’s build something real and fun:
// 🛍️ stores/cart.ts - Shopping cart with type safety
import { defineStore } from 'pinia';
interface Product {
id: string;
name: string;
price: number;
emoji: string;
inStock: boolean;
}
interface CartItem extends Product {
quantity: number;
}
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
isLoading: false,
discountCode: '' as string
}),
getters: {
// 💰 Calculate total price
totalPrice: (state) => {
return state.items.reduce((total, item) =>
total + (item.price * item.quantity), 0
);
},
// 📦 Total items count
itemCount: (state) => {
return state.items.reduce((count, item) =>
count + item.quantity, 0
);
},
// 🎉 Formatted summary
cartSummary: (state) => {
if (state.items.length === 0) return 'Your cart is empty! 🛒';
return `🛒 ${state.itemCount} items • $${state.totalPrice.toFixed(2)}`;
},
// 🔍 Check if product is in cart
isInCart: (state) => {
return (productId: string) =>
state.items.some(item => item.id === productId);
}
},
actions: {
// ➕ Add product to cart
async addToCart(product: Product) {
this.isLoading = true;
try {
const existingItem = this.items.find(item => item.id === product.id);
if (existingItem) {
// 📈 Increase quantity
existingItem.quantity++;
console.log(`📈 Updated ${product.emoji} ${product.name} quantity!`);
} else {
// 🆕 Add new item
this.items.push({ ...product, quantity: 1 });
console.log(`🎉 Added ${product.emoji} ${product.name} to cart!`);
}
} catch (error) {
console.error('❌ Failed to add item:', error);
} finally {
this.isLoading = false;
}
},
// 🗑️ Remove item from cart
removeFromCart(productId: string) {
const index = this.items.findIndex(item => item.id === productId);
if (index > -1) {
const item = this.items[index];
this.items.splice(index, 1);
console.log(`🗑️ Removed ${item.emoji} ${item.name} from cart!`);
}
},
// 🔄 Update quantity
updateQuantity(productId: string, quantity: number) {
const item = this.items.find(item => item.id === productId);
if (item) {
if (quantity <= 0) {
this.removeFromCart(productId);
} else {
item.quantity = quantity;
console.log(`🔄 Updated quantity to ${quantity}!`);
}
}
},
// 🧹 Clear entire cart
clearCart() {
this.items = [];
this.discountCode = '';
console.log('🧹 Cart cleared! Ready for new adventures! ✨');
}
}
});
🎮 Example 2: User Authentication Store
Let’s handle user authentication with style:
// 👤 stores/auth.ts - User authentication store
import { defineStore } from 'pinia';
interface User {
id: string;
username: string;
email: string;
avatar: string;
role: 'user' | 'admin';
}
interface LoginCredentials {
email: string;
password: string;
}
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
token: localStorage.getItem('token') || '',
isLoading: false,
error: '' as string
}),
getters: {
// 🔐 Check if user is logged in
isAuthenticated: (state) => !!state.token && !!state.user,
// 👑 Check if user is admin
isAdmin: (state) => state.user?.role === 'admin',
// 🎨 User display name
displayName: (state) => {
if (!state.user) return 'Guest 👋';
return `${state.user.username} ${state.user.avatar}`;
},
// 🚨 Has authentication error
hasError: (state) => !!state.error
},
actions: {
// 🔑 Login user
async login(credentials: LoginCredentials) {
this.isLoading = true;
this.error = '';
try {
// 🌐 Simulated API call
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('Invalid credentials! 🚫');
}
const data = await response.json();
// 🎉 Store user data
this.user = data.user;
this.token = data.token;
localStorage.setItem('token', data.token);
console.log(`🎉 Welcome back, ${this.user.username}! ${this.user.avatar}`);
} catch (error) {
this.error = error instanceof Error ? error.message : 'Login failed! 😰';
console.error('❌ Login error:', this.error);
} finally {
this.isLoading = false;
}
},
// 📝 Register new user
async register(userData: Omit<User, 'id'> & { password: string }) {
this.isLoading = true;
this.error = '';
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error('Registration failed! 😰');
}
const data = await response.json();
console.log('🎉 Registration successful! Welcome aboard! 🚀');
// 🔑 Auto-login after registration
await this.login({
email: userData.email,
password: userData.password
});
} catch (error) {
this.error = error instanceof Error ? error.message : 'Registration failed!';
} finally {
this.isLoading = false;
}
},
// 🚪 Logout user
logout() {
this.user = null;
this.token = '';
this.error = '';
localStorage.removeItem('token');
console.log('👋 Logged out successfully! See you soon! ✨');
},
// 🔄 Clear error
clearError() {
this.error = '';
}
}
});
🚀 Advanced Concepts
🧙♂️ Composition API Style Stores
For the modern Vue developers, here’s the Composition API approach:
// 🎯 stores/todos.ts - Composition API style
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
interface Todo {
id: string;
text: string;
completed: boolean;
emoji: string;
priority: 'low' | 'medium' | 'high';
}
export const useTodosStore = defineStore('todos', () => {
// 📊 State using refs
const todos = ref<Todo[]>([]);
const filter = ref<'all' | 'active' | 'completed'>('all');
const isLoading = ref(false);
// 📖 Getters using computed
const completedTodos = computed(() =>
todos.value.filter(todo => todo.completed)
);
const activeTodos = computed(() =>
todos.value.filter(todo => !todo.completed)
);
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active': return activeTodos.value;
case 'completed': return completedTodos.value;
default: return todos.value;
}
});
const stats = computed(() => ({
total: todos.value.length,
completed: completedTodos.value.length,
active: activeTodos.value.length,
completionRate: todos.value.length > 0
? Math.round((completedTodos.value.length / todos.value.length) * 100)
: 0
}));
// 🎬 Actions - regular functions
const addTodo = (text: string, emoji: string = '📝') => {
const newTodo: Todo = {
id: Date.now().toString(),
text,
completed: false,
emoji,
priority: 'medium'
};
todos.value.push(newTodo);
console.log(`✅ Added: ${emoji} ${text}`);
};
const toggleTodo = (id: string) => {
const todo = todos.value.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
const status = todo.completed ? 'completed' : 'active';
console.log(`🔄 Todo ${status}: ${todo.emoji} ${todo.text}`);
}
};
const deleteTodo = (id: string) => {
const index = todos.value.findIndex(t => t.id === id);
if (index > -1) {
const todo = todos.value[index];
todos.value.splice(index, 1);
console.log(`🗑️ Deleted: ${todo.emoji} ${todo.text}`);
}
};
const setFilter = (newFilter: typeof filter.value) => {
filter.value = newFilter;
console.log(`🔍 Filter set to: ${newFilter}`);
};
// 🎯 Return everything (like export)
return {
// State
todos,
filter,
isLoading,
// Getters
completedTodos,
activeTodos,
filteredTodos,
stats,
// Actions
addTodo,
toggleTodo,
deleteTodo,
setFilter
};
});
🏗️ Store Composition and Plugins
Advanced patterns for power users:
// 🔌 Store plugins for extra functionality
import { defineStore } from 'pinia';
// 📊 Analytics plugin
const analyticsPlugin = (context: any) => {
// 📝 Track all actions
context.store.$onAction(({ name, args }) => {
console.log(`📊 Action: ${name}`, args);
// Send to analytics service
});
};
// 💾 Persistence plugin
const persistencePlugin = (context: any) => {
const storeId = context.store.$id;
// 📥 Load from localStorage
const saved = localStorage.getItem(`pinia-${storeId}`);
if (saved) {
context.store.$patch(JSON.parse(saved));
}
// 💾 Save on changes
context.store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${storeId}`, JSON.stringify(state));
});
};
// 🏪 Store that uses other stores
export const useAppStore = defineStore('app', {
state: () => ({
theme: 'light' as 'light' | 'dark',
notifications: [] as string[]
}),
actions: {
// 🎨 Toggle theme and notify user
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
// 🔔 Use auth store to check if user is logged in
const authStore = useAuthStore();
if (authStore.isAuthenticated) {
this.addNotification(`🎨 Theme changed to ${this.theme} mode!`);
}
},
// 🔔 Add notification
addNotification(message: string) {
this.notifications.push(message);
setTimeout(() => {
this.removeNotification(message);
}, 3000);
},
// 🗑️ Remove notification
removeNotification(message: string) {
const index = this.notifications.indexOf(message);
if (index > -1) {
this.notifications.splice(index, 1);
}
}
}
});
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Destructuring Store State
// ❌ Wrong - loses reactivity!
const { count, name } = useCounterStore();
// These won't update when store changes! 😰
// ✅ Correct - preserve reactivity
import { storeToRefs } from 'pinia';
const store = useCounterStore();
const { count, name } = storeToRefs(store); // 🎯 Now they're reactive!
// ✅ Alternative - use the store directly
const store = useCounterStore();
// Use store.count, store.name in templates
🤯 Pitfall 2: Calling Actions in Getters
// ❌ Wrong - actions in getters!
getters: {
processedData: (state) => {
this.loadData(); // 💥 Don't call actions in getters!
return state.data.map(item => item.processed);
}
}
// ✅ Correct - keep getters pure
getters: {
processedData: (state) => {
return state.data.map(item => item.processed); // ✨ Pure function
}
}
// 🎯 Call actions in components or other actions
actions: {
async initializeData() {
await this.loadData();
// Now processedData getter will work with loaded data
}
}
🚫 Pitfall 3: Forgetting Async/Await
// ❌ Wrong - not handling promises
actions: {
loadUser(id: string) {
fetchUser(id).then(user => {
this.user = user; // 😰 This might not work as expected
});
}
}
// ✅ Correct - proper async handling
actions: {
async loadUser(id: string) {
try {
this.isLoading = true;
this.user = await fetchUser(id); // 🎯 Clean and predictable
console.log('✅ User loaded successfully!');
} catch (error) {
console.error('❌ Failed to load user:', error);
this.error = 'Failed to load user';
} finally {
this.isLoading = false;
}
}
}
🛠️ Best Practices
- 🎯 One Responsibility: Each store should handle one domain (users, cart, etc.)
- 📝 Descriptive Names: Use clear, descriptive names for stores and actions
- 🛡️ Type Everything: Define interfaces for your state and data structures
- 🔄 Handle Loading States: Always show users when operations are in progress
- 🚨 Error Handling: Catch and handle errors gracefully
- 🧪 Test Your Stores: Write tests for your store logic
- 📊 Use DevTools: Leverage Vue DevTools for debugging
- 💾 Persist Important Data: Save critical state to localStorage when needed
🧪 Hands-On Exercise
🎯 Challenge: Build a Recipe Book Store
Create a complete recipe management system with Pinia:
📋 Requirements:
- ✅ Store recipes with ingredients, instructions, and ratings
- 🏷️ Categories (breakfast, lunch, dinner, dessert)
- ❤️ Favorites system
- 🔍 Search and filter functionality
- 📊 Statistics (total recipes, average rating)
- 🎨 Each recipe needs an emoji!
🚀 Bonus Points:
- Add ingredient shopping list generator
- Implement recipe sharing functionality
- Create meal planning features
- Add cooking timer integration
💡 Solution
🔍 Click to see solution
// 🍳 stores/recipes.ts - Complete recipe management
import { defineStore } from 'pinia';
interface Recipe {
id: string;
title: string;
emoji: string;
category: 'breakfast' | 'lunch' | 'dinner' | 'dessert';
ingredients: string[];
instructions: string[];
prepTime: number; // in minutes
cookTime: number;
servings: number;
rating: number; // 1-5 stars
isFavorite: boolean;
tags: string[];
difficulty: 'easy' | 'medium' | 'hard';
createdAt: Date;
}
export const useRecipesStore = defineStore('recipes', {
state: () => ({
recipes: [] as Recipe[],
searchQuery: '',
selectedCategory: 'all' as Recipe['category'] | 'all',
showFavoritesOnly: false,
isLoading: false,
currentRecipe: null as Recipe | null
}),
getters: {
// 🔍 Filtered recipes based on search and filters
filteredRecipes: (state) => {
let filtered = state.recipes;
// 🔍 Apply search filter
if (state.searchQuery) {
filtered = filtered.filter(recipe =>
recipe.title.toLowerCase().includes(state.searchQuery.toLowerCase()) ||
recipe.ingredients.some(ingredient =>
ingredient.toLowerCase().includes(state.searchQuery.toLowerCase())
)
);
}
// 🏷️ Apply category filter
if (state.selectedCategory !== 'all') {
filtered = filtered.filter(recipe => recipe.category === state.selectedCategory);
}
// ❤️ Apply favorites filter
if (state.showFavoritesOnly) {
filtered = filtered.filter(recipe => recipe.isFavorite);
}
return filtered;
},
// ❤️ Favorite recipes
favoriteRecipes: (state) => state.recipes.filter(recipe => recipe.isFavorite),
// 📊 Recipe statistics
stats: (state) => {
const totalRecipes = state.recipes.length;
const averageRating = totalRecipes > 0
? state.recipes.reduce((sum, recipe) => sum + recipe.rating, 0) / totalRecipes
: 0;
const categoryStats = state.recipes.reduce((stats, recipe) => {
stats[recipe.category] = (stats[recipe.category] || 0) + 1;
return stats;
}, {} as Record<Recipe['category'], number>);
return {
total: totalRecipes,
favorites: state.recipes.filter(r => r.isFavorite).length,
averageRating: Math.round(averageRating * 10) / 10,
byCategory: categoryStats,
totalCookTime: state.recipes.reduce((total, recipe) =>
total + recipe.prepTime + recipe.cookTime, 0
)
};
},
// 🛒 Shopping list from selected recipes
shoppingList: (state) => {
const selectedRecipes = state.recipes.filter(recipe => recipe.isFavorite);
const allIngredients = selectedRecipes.flatMap(recipe => recipe.ingredients);
// 📝 Remove duplicates and sort
return [...new Set(allIngredients)].sort();
},
// 🎯 Recipe recommendations
recommendedRecipes: (state) => {
return state.recipes
.filter(recipe => recipe.rating >= 4)
.sort((a, b) => b.rating - a.rating)
.slice(0, 5);
}
},
actions: {
// ➕ Add new recipe
addRecipe(recipeData: Omit<Recipe, 'id' | 'createdAt'>) {
const newRecipe: Recipe = {
...recipeData,
id: Date.now().toString(),
createdAt: new Date()
};
this.recipes.push(newRecipe);
console.log(`🍳 Added recipe: ${newRecipe.emoji} ${newRecipe.title}!`);
},
// ✏️ Update recipe
updateRecipe(id: string, updates: Partial<Recipe>) {
const recipe = this.recipes.find(r => r.id === id);
if (recipe) {
Object.assign(recipe, updates);
console.log(`✏️ Updated recipe: ${recipe.emoji} ${recipe.title}!`);
}
},
// 🗑️ Delete recipe
deleteRecipe(id: string) {
const index = this.recipes.findIndex(r => r.id === id);
if (index > -1) {
const recipe = this.recipes[index];
this.recipes.splice(index, 1);
console.log(`🗑️ Deleted recipe: ${recipe.emoji} ${recipe.title}!`);
}
},
// ❤️ Toggle favorite
toggleFavorite(id: string) {
const recipe = this.recipes.find(r => r.id === id);
if (recipe) {
recipe.isFavorite = !recipe.isFavorite;
const status = recipe.isFavorite ? 'added to' : 'removed from';
console.log(`❤️ Recipe ${status} favorites: ${recipe.emoji} ${recipe.title}!`);
}
},
// ⭐ Rate recipe
rateRecipe(id: string, rating: number) {
const recipe = this.recipes.find(r => r.id === id);
if (recipe && rating >= 1 && rating <= 5) {
recipe.rating = rating;
console.log(`⭐ Rated ${recipe.emoji} ${recipe.title}: ${rating} stars!`);
}
},
// 🔍 Set search query
setSearchQuery(query: string) {
this.searchQuery = query;
},
// 🏷️ Set category filter
setCategoryFilter(category: Recipe['category'] | 'all') {
this.selectedCategory = category;
},
// ❤️ Toggle favorites filter
toggleFavoritesFilter() {
this.showFavoritesOnly = !this.showFavoritesOnly;
},
// 📥 Load sample recipes
loadSampleRecipes() {
const sampleRecipes: Omit<Recipe, 'id' | 'createdAt'>[] = [
{
title: 'Perfect Pancakes',
emoji: '🥞',
category: 'breakfast',
ingredients: ['2 cups flour', '2 eggs', '1 cup milk', '2 tbsp sugar'],
instructions: ['Mix dry ingredients', 'Add wet ingredients', 'Cook on griddle'],
prepTime: 10,
cookTime: 15,
servings: 4,
rating: 5,
isFavorite: true,
tags: ['easy', 'family-friendly'],
difficulty: 'easy'
},
{
title: 'Chocolate Chip Cookies',
emoji: '🍪',
category: 'dessert',
ingredients: ['2 cups flour', '1 cup butter', '1 cup brown sugar', '2 eggs', '1 cup chocolate chips'],
instructions: ['Cream butter and sugar', 'Add eggs', 'Mix in flour', 'Fold in chocolate chips', 'Bake at 350°F'],
prepTime: 15,
cookTime: 12,
servings: 24,
rating: 4,
isFavorite: false,
tags: ['sweet', 'baking'],
difficulty: 'medium'
}
];
sampleRecipes.forEach(recipe => this.addRecipe(recipe));
console.log('📚 Sample recipes loaded! Happy cooking! 🍳');
}
}
});
🎓 Key Takeaways
You’ve mastered the art of Pinia! Here’s what you can now do:
- ✅ Create modern stores with clean, intuitive syntax 💪
- ✅ Manage complex state across your Vue applications 🎯
- ✅ Handle async operations with proper error handling 🛡️
- ✅ Use TypeScript for bulletproof state management 🚀
- ✅ Compose stores and create reusable patterns 🧩
- ✅ Debug effectively with Vue DevTools integration 🔍
Remember: Pinia makes state management enjoyable! It’s simple, powerful, and works beautifully with TypeScript. 🍍✨
🤝 Next Steps
Congratulations! 🎉 You’ve become a Pinia master!
Here’s what to do next:
- 💻 Build a real project using Pinia stores
- 🧪 Experiment with store composition patterns
- 📚 Explore Pinia plugins and advanced features
- 🌟 Share your Pinia creations with the community!
Keep building amazing Vue applications with type-safe state management! The pineapple power is now yours! 🍍🚀
Happy coding with Pinia! 🎉🍍✨