Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand Zustand fundamentals ๐ฏ
- Apply Zustand in real projects ๐๏ธ
- Debug common Zustand issues ๐
- Write type-safe state management code โจ
๐ฏ Introduction
Welcome to the world of Zustand! ๐ In this tutorial, weโll explore the simplest and most delightful state management library for React applications.
Zustand (German for โstateโ) is like having a lightweight, flexible toolbox ๐งฐ for managing your appโs data. No complex setup, no boilerplate code, just pure simplicity that scales with your needs!
By the end of this tutorial, youโll be building robust, type-safe applications with Zustand. Letโs dive in! ๐โโ๏ธ
๐ Understanding Zustand
๐ค What is Zustand?
Zustand is like a smart sticky note system ๐ for your React app. Imagine having a magical notepad where any component can write notes, read them, and get notified when someone else updates them!
In TypeScript terms, Zustand provides a minimal, hook-based state management solution thatโs:
- โจ Simple: No providers, no reducers, just functions
- ๐ Fast: Minimal re-renders and optimized performance
- ๐ก๏ธ Type-safe: First-class TypeScript support
- ๐ฆ Tiny: Under 3kb gzipped
๐ก Why Use Zustand?
Hereโs why developers are switching to Zustand:
- Zero Boilerplate ๐ฏ: Write less, do more
- React-like Logic โ๏ธ: Familiar patterns and concepts
- Excellent DevTools ๐: Debug state changes easily
- Framework Agnostic ๐: Use with React, Vue, or vanilla JS
- Flexible Architecture ๐๏ธ: Organize state however you want
Real-world example: Building a shopping app ๐. With Zustand, managing cart items, user preferences, and UI state becomes effortless!
๐ง Basic Syntax and Usage
๐ Installation & Setup
Letโs get started:
# ๐ฆ Install Zustand
npm install zustand
# or with pnpm
pnpm add zustand
๐จ Creating Your First Store
Hereโs the magic:
// ๐ช Create a simple store
import { create } from 'zustand';
interface CounterState {
count: number; // ๐ข Current count
increment: () => void; // โ Add one
decrement: () => void; // โ Subtract one
reset: () => void; // ๐ Back to zero
}
// โจ Create the store with TypeScript magic!
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
๐ฏ Using the Store in Components
Now the fun part:
// ๐ฎ Counter component
import React from 'react';
const Counter: React.FC = () => {
// ๐ช Hook into the store
const { count, increment, decrement, reset } = useCounterStore();
return (
<div className="counter">
<h2>Count: {count} ๐ฏ</h2>
<div className="buttons">
<button onClick={increment}>โ Add</button>
<button onClick={decrement}>โ Subtract</button>
<button onClick={reset}>๐ Reset</button>
</div>
</div>
);
};
๐ก Pro Tip: Zustand automatically triggers re-renders only when the data youโre using changes!
๐ก Practical Examples
๐ Example 1: Shopping Cart System
Letโs build something real:
// ๐๏ธ Shopping cart store
interface Product {
id: string;
name: string;
price: number;
emoji: string;
}
interface CartItem extends Product {
quantity: number;
}
interface CartState {
items: CartItem[];
totalItems: number;
totalPrice: number;
addItem: (product: Product) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
}
const useCartStore = create<CartState>((set, get) => ({
items: [],
totalItems: 0,
totalPrice: 0,
// โ Add item to cart
addItem: (product) => set((state) => {
const existingItem = state.items.find(item => item.id === product.id);
if (existingItem) {
// ๐ Increase quantity
const updatedItems = state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
return {
items: updatedItems,
totalItems: state.totalItems + 1,
totalPrice: state.totalPrice + product.price,
};
} else {
// ๐ Add new item
const newItem: CartItem = { ...product, quantity: 1 };
return {
items: [...state.items, newItem],
totalItems: state.totalItems + 1,
totalPrice: state.totalPrice + product.price,
};
}
}),
// ๐๏ธ Remove item completely
removeItem: (id) => set((state) => {
const item = state.items.find(item => item.id === id);
if (!item) return state;
return {
items: state.items.filter(item => item.id !== id),
totalItems: state.totalItems - item.quantity,
totalPrice: state.totalPrice - (item.price * item.quantity),
};
}),
// ๐ Update quantity
updateQuantity: (id, quantity) => set((state) => {
if (quantity <= 0) {
// ๐๏ธ Remove if quantity is 0
return get().removeItem(id);
}
const updatedItems = state.items.map(item => {
if (item.id === id) {
const quantityDiff = quantity - item.quantity;
return { ...item, quantity };
}
return item;
});
const item = state.items.find(item => item.id === id);
if (!item) return state;
const quantityDiff = quantity - item.quantity;
return {
items: updatedItems,
totalItems: state.totalItems + quantityDiff,
totalPrice: state.totalPrice + (item.price * quantityDiff),
};
}),
// ๐งน Clear entire cart
clearCart: () => set({
items: [],
totalItems: 0,
totalPrice: 0,
}),
}));
// ๐ Cart component
const ShoppingCart: React.FC = () => {
const { items, totalItems, totalPrice, addItem, removeItem } = useCartStore();
const sampleProducts: Product[] = [
{ id: '1', name: 'TypeScript Book', price: 29.99, emoji: '๐' },
{ id: '2', name: 'Coffee Mug', price: 12.99, emoji: 'โ' },
{ id: '3', name: 'Laptop Sticker', price: 4.99, emoji: '๐ป' },
];
return (
<div className="shopping-cart">
<h2>๐ Shopping Cart ({totalItems} items)</h2>
{/* ๐๏ธ Product catalog */}
<div className="products">
<h3>Available Products:</h3>
{sampleProducts.map(product => (
<div key={product.id} className="product">
<span>{product.emoji} {product.name} - ${product.price}</span>
<button onClick={() => addItem(product)}>โ Add to Cart</button>
</div>
))}
</div>
{/* ๐ Cart items */}
<div className="cart-items">
<h3>Cart Items:</h3>
{items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.emoji} {item.name} x{item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button onClick={() => removeItem(item.id)}>๐๏ธ Remove</button>
</div>
))}
</div>
{/* ๐ฐ Total */}
<div className="total">
<strong>Total: ${totalPrice.toFixed(2)} ๐ฐ</strong>
</div>
</div>
);
};
๐ฎ Example 2: Game State Manager
Letโs make it fun:
// ๐ Game state management
interface Player {
id: string;
name: string;
score: number;
level: number;
achievements: string[];
}
interface GameState {
currentPlayer: Player | null;
isPlaying: boolean;
gameMode: 'easy' | 'medium' | 'hard';
highScores: Player[];
// ๐ฎ Game actions
startGame: (playerName: string) => void;
endGame: () => void;
addScore: (points: number) => void;
levelUp: () => void;
addAchievement: (achievement: string) => void;
setGameMode: (mode: 'easy' | 'medium' | 'hard') => void;
}
const useGameStore = create<GameState>((set, get) => ({
currentPlayer: null,
isPlaying: false,
gameMode: 'easy',
highScores: [],
// ๐ Start new game
startGame: (playerName) => set({
currentPlayer: {
id: Date.now().toString(),
name: playerName,
score: 0,
level: 1,
achievements: ['๐ First Steps'],
},
isPlaying: true,
}),
// ๐ End current game
endGame: () => set((state) => {
if (!state.currentPlayer) return state;
// ๐ Update high scores
const updatedHighScores = [...state.highScores, state.currentPlayer]
.sort((a, b) => b.score - a.score)
.slice(0, 10); // Keep top 10
return {
currentPlayer: null,
isPlaying: false,
highScores: updatedHighScores,
};
}),
// โญ Add points
addScore: (points) => set((state) => {
if (!state.currentPlayer) return state;
const newScore = state.currentPlayer.score + points;
const updated = { ...state.currentPlayer, score: newScore };
// ๐ Auto level up every 1000 points
if (Math.floor(newScore / 1000) > Math.floor(state.currentPlayer.score / 1000)) {
get().levelUp();
}
return { currentPlayer: updated };
}),
// ๐ Level up
levelUp: () => set((state) => {
if (!state.currentPlayer) return state;
const newLevel = state.currentPlayer.level + 1;
const levelAchievement = `๐ Level ${newLevel} Master`;
return {
currentPlayer: {
...state.currentPlayer,
level: newLevel,
achievements: [...state.currentPlayer.achievements, levelAchievement],
},
};
}),
// ๐๏ธ Add achievement
addAchievement: (achievement) => set((state) => {
if (!state.currentPlayer) return state;
return {
currentPlayer: {
...state.currentPlayer,
achievements: [...state.currentPlayer.achievements, achievement],
},
};
}),
// โ๏ธ Set difficulty
setGameMode: (mode) => set({ gameMode: mode }),
}));
๐ Advanced Concepts
๐งโโ๏ธ Slices: Organizing Large Stores
When your store gets big, split it into slices:
// ๐ฏ User slice
interface UserSlice {
user: { name: string; email: string } | null;
login: (name: string, email: string) => void;
logout: () => void;
}
const createUserSlice = (set: any): UserSlice => ({
user: null,
login: (name, email) => set({ user: { name, email } }),
logout: () => set({ user: null }),
});
// ๐ Cart slice
interface CartSlice {
items: string[];
addItem: (item: string) => void;
clearCart: () => void;
}
const createCartSlice = (set: any, get: any): CartSlice => ({
items: [],
addItem: (item) => set((state: any) => ({ items: [...state.items, item] })),
clearCart: () => set({ items: [] }),
});
// ๐ Combine slices
type AppState = UserSlice & CartSlice;
const useAppStore = create<AppState>((set, get) => ({
...createUserSlice(set),
...createCartSlice(set, get),
}));
๐๏ธ Middleware: Superpowers for Your Store
Add logging, persistence, and more:
// ๐ With logging middleware
import { subscribeWithSelector } from 'zustand/middleware';
const useStoreWithLogging = create<CounterState>()(
subscribeWithSelector((set, get) => ({
count: 0,
increment: () => {
console.log('๐ Incrementing count from', get().count);
set((state) => ({ count: state.count + 1 }));
},
decrement: () => {
console.log('๐ Decrementing count from', get().count);
set((state) => ({ count: state.count - 1 }));
},
reset: () => {
console.log('๐ Resetting count');
set({ count: 0 });
},
}))
);
// ๐พ With persistence
import { persist } from 'zustand/middleware';
const usePersistentStore = create<CounterState>()(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}),
{
name: 'counter-storage', // ๐ท๏ธ localStorage key
}
)
);
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Mutating State Directly
// โ Wrong way - direct mutation!
const useBadStore = create<{ items: string[] }>((set) => ({
items: [],
addItem: (item) => set((state) => {
state.items.push(item); // ๐ฅ Mutating state directly!
return state;
}),
}));
// โ
Correct way - immutable updates!
const useGoodStore = create<{ items: string[] }>((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item], // โจ Creating new array
})),
}));
๐คฏ Pitfall 2: Overusing Selectors
// โ Inefficient - selecting entire store
const MyComponent = () => {
const store = useStore(); // ๐ฐ Re-renders on ANY change
return <div>{store.count}</div>;
};
// โ
Efficient - selecting only what you need
const MyComponent = () => {
const count = useStore((state) => state.count); // ๐ฏ Only re-renders when count changes
return <div>{count}</div>;
};
๐ซ Pitfall 3: Async Actions Without Proper Error Handling
// โ Dangerous - no error handling
const useApiStore = create<{
data: any;
fetchData: () => void;
}>((set) => ({
data: null,
fetchData: async () => {
const response = await fetch('/api/data'); // ๐ฅ Could fail!
const data = await response.json();
set({ data });
},
}));
// โ
Safe - proper error handling
interface ApiState {
data: any;
loading: boolean;
error: string | null;
fetchData: () => Promise<void>;
}
const useApiStore = create<ApiState>((set) => ({
data: null,
loading: false,
error: null,
fetchData: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
set({ data, loading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Unknown error',
loading: false
});
}
},
}));
๐ ๏ธ Best Practices
- ๐ฏ Keep Stores Focused: One store per domain (users, cart, settings)
- ๐ Use TypeScript: Define clear interfaces for your state
- ๐ Immutable Updates: Always create new objects/arrays
- โก Selective Subscriptions: Only subscribe to data you need
- ๐งช Test Your Stores: Write unit tests for store logic
- ๐ฆ Use Middleware: Leverage persistence, logging, devtools
- ๐๏ธ Split Large Stores: Use slices for better organization
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Todo App with Categories
Create a feature-rich todo application:
๐ Requirements:
- โ Add, edit, delete todos
- ๐ท๏ธ Categorize todos (work, personal, urgent)
- ๐ฏ Filter by category and completion status
- ๐ Display statistics (total, completed, pending)
- ๐พ Persist data to localStorage
- ๐จ Each todo needs an emoji!
๐ Bonus Points:
- Add due dates with reminders
- Implement priority levels
- Create drag-and-drop reordering
- Add search functionality
๐ก Solution
๐ Click to see solution
// ๐ฏ Complete todo app with Zustand!
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface Todo {
id: string;
title: string;
completed: boolean;
category: 'work' | 'personal' | 'urgent';
emoji: string;
dueDate?: Date;
priority: 'low' | 'medium' | 'high';
createdAt: Date;
}
interface TodoFilter {
category?: Todo['category'];
completed?: boolean;
priority?: Todo['priority'];
}
interface TodoState {
todos: Todo[];
filter: TodoFilter;
searchQuery: string;
// ๐ Computed properties
totalTodos: number;
completedTodos: number;
pendingTodos: number;
filteredTodos: Todo[];
// ๐ ๏ธ Actions
addTodo: (todo: Omit<Todo, 'id' | 'createdAt'>) => void;
editTodo: (id: string, updates: Partial<Todo>) => void;
deleteTodo: (id: string) => void;
toggleTodo: (id: string) => void;
setFilter: (filter: TodoFilter) => void;
setSearchQuery: (query: string) => void;
clearCompleted: () => void;
getStats: () => { total: number; completed: number; pending: number };
}
const useTodoStore = create<TodoState>()(
persist(
(set, get) => ({
todos: [],
filter: {},
searchQuery: '',
// ๐ Computed getters
get totalTodos() {
return get().todos.length;
},
get completedTodos() {
return get().todos.filter(todo => todo.completed).length;
},
get pendingTodos() {
return get().todos.filter(todo => !todo.completed).length;
},
get filteredTodos() {
const { todos, filter, searchQuery } = get();
return todos
.filter(todo => {
// ๐ท๏ธ Category filter
if (filter.category && todo.category !== filter.category) return false;
// โ
Completion filter
if (filter.completed !== undefined && todo.completed !== filter.completed) return false;
// ๐ฏ Priority filter
if (filter.priority && todo.priority !== filter.priority) return false;
// ๐ Search filter
if (searchQuery && !todo.title.toLowerCase().includes(searchQuery.toLowerCase())) return false;
return true;
})
.sort((a, b) => {
// ๐
Sort by priority, then by due date
const priorityOrder = { high: 3, medium: 2, low: 1 };
if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
return priorityOrder[b.priority] - priorityOrder[a.priority];
}
if (a.dueDate && b.dueDate) {
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
}
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
},
// โ Add new todo
addTodo: (todoData) => set((state) => ({
todos: [
...state.todos,
{
...todoData,
id: Date.now().toString(),
createdAt: new Date(),
},
],
})),
// โ๏ธ Edit existing todo
editTodo: (id, updates) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, ...updates } : todo
),
})),
// ๐๏ธ Delete todo
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id),
})),
// ๐ Toggle completion
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
// ๐ท๏ธ Set filter
setFilter: (filter) => set({ filter }),
// ๐ Set search query
setSearchQuery: (searchQuery) => set({ searchQuery }),
// ๐งน Clear completed todos
clearCompleted: () => set((state) => ({
todos: state.todos.filter(todo => !todo.completed),
})),
// ๐ Get statistics
getStats: () => {
const { totalTodos, completedTodos, pendingTodos } = get();
return { total: totalTodos, completed: completedTodos, pending: pendingTodos };
},
}),
{
name: 'todo-app-storage', // ๐พ localStorage key
}
)
);
// ๐ฑ Todo App Component
const TodoApp: React.FC = () => {
const {
filteredTodos,
filter,
searchQuery,
addTodo,
editTodo,
deleteTodo,
toggleTodo,
setFilter,
setSearchQuery,
clearCompleted,
getStats,
} = useTodoStore();
const stats = getStats();
const handleAddTodo = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
addTodo({
title: formData.get('title') as string,
category: formData.get('category') as Todo['category'],
priority: formData.get('priority') as Todo['priority'],
emoji: formData.get('emoji') as string,
completed: false,
dueDate: formData.get('dueDate') ? new Date(formData.get('dueDate') as string) : undefined,
});
e.currentTarget.reset();
};
return (
<div className="todo-app">
<h1>๐ Todo App with Zustand</h1>
{/* ๐ Statistics */}
<div className="stats">
<span>๐ Total: {stats.total}</span>
<span>โ
Completed: {stats.completed}</span>
<span>โณ Pending: {stats.pending}</span>
</div>
{/* โ Add Todo Form */}
<form onSubmit={handleAddTodo} className="add-todo-form">
<input name="title" placeholder="What needs to be done? ๐ค" required />
<input name="emoji" placeholder="๐" maxLength={2} />
<select name="category" required>
<option value="">Select Category</option>
<option value="work">๐ผ Work</option>
<option value="personal">๐ค Personal</option>
<option value="urgent">๐จ Urgent</option>
</select>
<select name="priority" required>
<option value="low">๐ข Low</option>
<option value="medium">๐ก Medium</option>
<option value="high">๐ด High</option>
</select>
<input name="dueDate" type="date" />
<button type="submit">โ Add Todo</button>
</form>
{/* ๐ Search and Filters */}
<div className="filters">
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="๐ Search todos..."
/>
<select
onChange={(e) => setFilter({ ...filter, category: e.target.value as Todo['category'] || undefined })}
>
<option value="">All Categories</option>
<option value="work">๐ผ Work</option>
<option value="personal">๐ค Personal</option>
<option value="urgent">๐จ Urgent</option>
</select>
<button onClick={clearCompleted}>๐งน Clear Completed</button>
</div>
{/* ๐ Todo List */}
<div className="todo-list">
{filteredTodos.map(todo => (
<div key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span className="todo-content">
{todo.emoji} {todo.title}
<small>
๐ท๏ธ {todo.category} | ๐ฏ {todo.priority}
{todo.dueDate && ` | ๐
${new Date(todo.dueDate).toLocaleDateString()}`}
</small>
</span>
<button onClick={() => deleteTodo(todo.id)}>๐๏ธ</button>
</div>
))}
</div>
{filteredTodos.length === 0 && (
<div className="empty-state">
<p>๐ No todos found! Time to add some tasks or adjust your filters.</p>
</div>
)}
</div>
);
};
๐ Key Takeaways
Youโve mastered Zustand! Hereโs what you can now do:
- โ Create lightweight stores with minimal boilerplate ๐ช
- โ Manage complex state with type safety ๐ก๏ธ
- โ Optimize performance with selective subscriptions ๐
- โ Organize large applications with slices and middleware ๐๏ธ
- โ Build real-world features like shopping carts and todo apps! ๐ฏ
Remember: Zustand makes state management fun and simple. No complex setup, just pure functionality! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Zustand state management!
Hereโs what to do next:
- ๐ป Build a project using Zustand with the patterns you learned
- ๐งช Experiment with different middleware options
- ๐ Explore advanced Zustand patterns and recipes
- ๐ Try combining Zustand with other React libraries
- ๐ Share your Zustand knowledge with the community!
Remember: Great state management leads to great applications. Keep building amazing things! ๐
Happy state managing! ๐๐โจ