🎯 Ejemplos recomendados
Balanced sample collections from various categories for you to explore
Ejemplos de Zustand
Ejemplos de la biblioteca de gestión de estado Zustand incluyendo configuración básica, middleware, persistencia y patrones avanzados
💻 Store Básico de Zustand typescript
🟢 simple
⭐⭐
Patrones fundamentales de store Zustand incluyendo crear, actualizar y consumir estado en componentes React
⏱️ 20 min
🏷️ zustand, state-management, react, typescript
Prerequisites:
React basics, TypeScript, Hooks
// Zustand Basic Store Examples
// Zustand is a small, fast, and scalable state management solution for React
// 1. Basic Counter Store
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
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 }),
}))
// Usage in component
function Counter() {
const { count, increment, decrement, reset } = useCounterStore()
return (
<div className="p-4 border rounded-lg">
<h2 className="text-xl font-bold mb-4">Counter: {count}</h2>
<div className="space-x-2">
<button onClick={increment} className="px-4 py-2 bg-blue-500 text-white rounded">
Increment
</button>
<button onClick={decrement} className="px-4 py-2 bg-red-500 text-white rounded">
Decrement
</button>
<button onClick={reset} className="px-4 py-2 bg-gray-500 text-white rounded">
Reset
</button>
</div>
</div>
)
}
// 2. Todo Store with CRUD Operations
interface Todo {
id: string
text: string
completed: boolean
createdAt: Date
}
interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
deleteTodo: (id: string) => void
updateTodo: (id: string, text: string) => void
clearCompleted: () => void
completedCount: number
totalCount: number
}
const useTodoStore = create<TodoState>((set, get) => ({
todos: [],
addTodo: (text: string) => {
const newTodo: Todo = {
id: Date.now().toString(),
text,
completed: false,
createdAt: new Date(),
}
set((state) => ({ todos: [...state.todos, newTodo] }))
},
toggleTodo: (id: string) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id: string) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
updateTodo: (id: string, text: string) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, text } : todo
),
})),
clearCompleted: () =>
set((state) => ({
todos: state.todos.filter((todo) => !todo.completed),
})),
get completedCount() {
return get().todos.filter((todo) => todo.completed).length
},
get totalCount() {
return get().todos.length
},
}))
// Todo Component
function TodoList() {
const {
todos,
addTodo,
toggleTodo,
deleteTodo,
updateTodo,
clearCompleted,
completedCount,
totalCount,
} = useTodoStore()
const [newTodoText, setNewTodoText] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (newTodoText.trim()) {
addTodo(newTodoText.trim())
setNewTodoText('')
}
}
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Todo List</h1>
<form onSubmit={handleSubmit} className="mb-4">
<div className="flex space-x-2">
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Add a new todo..."
className="flex-1 px-3 py-2 border rounded"
/>
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">
Add
</button>
</div>
</form>
<div className="mb-4 text-sm text-gray-600">
{completedCount} of {totalCount} completed
</div>
<ul className="space-y-2">
{todos.map((todo) => (
<li key={todo.id} className="flex items-center space-x-2">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="w-4 h-4"
/>
<input
type="text"
value={todo.text}
onChange={(e) => updateTodo(todo.id, e.target.value)}
className={`flex-1 px-2 py-1 border rounded ${
todo.completed ? 'line-through text-gray-500' : ''
}`}
/>
<button
onClick={() => deleteTodo(todo.id)}
className="px-2 py-1 bg-red-500 text-white rounded text-sm"
>
Delete
</button>
</li>
))}
</ul>
{completedCount > 0 && (
<button
onClick={clearCompleted}
className="mt-4 px-4 py-2 bg-gray-500 text-white rounded"
>
Clear Completed ({completedCount})
</button>
)}
</div>
)
}
// 3. Shopping Cart Store
interface CartItem {
id: string
name: string
price: number
quantity: number
image?: string
}
interface CartState {
items: CartItem[]
addItem: (item: Omit<CartItem, 'quantity'>) => void
removeItem: (id: string) => void
updateQuantity: (id: string, quantity: number) => void
clearCart: () => void
getTotalPrice: () => number
getTotalItems: () => number
isInCart: (id: string) => boolean
}
const useCartStore = create<CartState>((set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existingItem = state.items.find((i) => i.id === item.id)
if (existingItem) {
return {
items: state.items.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
),
}
}
return { items: [...state.items, { ...item, quantity: 1 }] }
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
items: state.items.map((item) =>
item.id === id ? { ...item, quantity: Math.max(1, quantity) } : item
),
})),
clearCart: () => set({ items: [] }),
getTotalPrice: () =>
get().items.reduce((total, item) => total + item.price * item.quantity, 0),
getTotalItems: () =>
get().items.reduce((total, item) => total + item.quantity, 0),
isInCart: (id) => get().items.some((item) => item.id === id),
}))
// Cart Component
function ShoppingCart() {
const {
items,
addItem,
removeItem,
updateQuantity,
clearCart,
getTotalPrice,
getTotalItems,
} = useCartStore()
const sampleProducts = [
{ id: '1', name: 'Laptop', price: 999, image: '/laptop.jpg' },
{ id: '2', name: 'Mouse', price: 29, image: '/mouse.jpg' },
{ id: '3', name: 'Keyboard', price: 79, image: '/keyboard.jpg' },
]
return (
<div className="max-w-4xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>
<div className="grid md:grid-cols-2 gap-8">
{/* Products */}
<div>
<h2 className="text-xl font-semibold mb-4">Products</h2>
<div className="space-y-4">
{sampleProducts.map((product) => (
<div key={product.id} className="border p-4 rounded-lg">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</div>
<button
onClick={() => addItem(product)}
className="px-3 py-1 bg-blue-500 text-white rounded"
>
Add to Cart
</button>
</div>
</div>
))}
</div>
</div>
{/* Cart */}
<div>
<h2 className="text-xl font-semibold mb-4">
Cart ({getTotalItems()} items)
</h2>
{items.length === 0 ? (
<p className="text-gray-500">Your cart is empty</p>
) : (
<>
<div className="space-y-2 mb-4">
{items.map((item) => (
<div key={item.id} className="border p-3 rounded">
<div className="flex justify-between items-center">
<div>
<h4 className="font-semibold">{item.name}</h4>
<p className="text-gray-600">${item.price}</p>
</div>
<div className="flex items-center space-x-2">
<input
type="number"
min="1"
value={item.quantity}
onChange={(e) =>
updateQuantity(item.id, parseInt(e.target.value))
}
className="w-16 px-2 py-1 border rounded"
/>
<button
onClick={() => removeItem(item.id)}
className="px-2 py-1 bg-red-500 text-white rounded text-sm"
>
Remove
</button>
</div>
</div>
<p className="text-sm text-gray-600 mt-1">
Subtotal: ${(item.price * item.quantity).toFixed(2)}
</p>
</div>
))}
</div>
<div className="border-t pt-4">
<div className="flex justify-between text-lg font-semibold">
<span>Total:</span>
<span>${getTotalPrice().toFixed(2)}</span>
</div>
<div className="mt-4 space-x-2">
<button className="px-4 py-2 bg-green-500 text-white rounded">
Checkout
</button>
<button
onClick={clearCart}
className="px-4 py-2 bg-gray-500 text-white rounded"
>
Clear Cart
</button>
</div>
</div>
</>
)}
</div>
</div>
</div>
)
}
// 4. User Authentication Store
interface User {
id: string
name: string
email: string
avatar?: string
role: 'admin' | 'user'
}
interface AuthState {
user: User | null
isAuthenticated: boolean
isLoading: boolean
login: (email: string, password: string) => Promise<void>
logout: () => void
register: (name: string, email: string, password: string) => Promise<void>
updateProfile: (data: Partial<User>) => Promise<void>
}
const useAuthStore = create<AuthState>((set, get) => ({
user: null,
isAuthenticated: false,
isLoading: false,
login: async (email: string, password: string) => {
set({ isLoading: true })
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000))
// Mock successful login
const user: User = {
id: '1',
name: 'John Doe',
email,
role: 'user',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e',
}
set({ user, isAuthenticated: true, isLoading: false })
} catch (error) {
set({ isLoading: false })
throw error
}
},
logout: () => {
set({ user: null, isAuthenticated: false })
},
register: async (name: string, email: string, password: string) => {
set({ isLoading: true })
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000))
const user: User = {
id: Date.now().toString(),
name,
email,
role: 'user',
}
set({ user, isAuthenticated: true, isLoading: false })
} catch (error) {
set({ isLoading: false })
throw error
}
},
updateProfile: async (data: Partial<User>) => {
const currentUser = get().user
if (!currentUser) return
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 500))
const updatedUser = { ...currentUser, ...data }
set({ user: updatedUser })
} catch (error) {
throw error
}
},
}))
// Authentication Components
function LoginForm() {
const { login, isLoading } = useAuthStore()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await login(email, password)
} catch (error) {
alert('Login failed')
}
}
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-4">
<h2 className="text-xl font-bold mb-4">Login</h2>
<div className="space-y-4">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="w-full px-3 py-2 border rounded"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full px-3 py-2 border rounded"
required
/>
<button
type="submit"
disabled={isLoading}
className="w-full px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</div>
</form>
)
}
function UserProfile() {
const { user, logout, updateProfile } = useAuthStore()
const [isEditing, setIsEditing] = useState(false)
const [editName, setEditName] = useState(user?.name || '')
const handleUpdate = async () => {
try {
await updateProfile({ name: editName })
setIsEditing(false)
} catch (error) {
alert('Update failed')
}
}
return (
<div className="max-w-md mx-auto p-4">
<h2 className="text-xl font-bold mb-4">Profile</h2>
<div className="space-y-4">
{user?.avatar && (
<img
src={user.avatar}
alt={user.name}
className="w-20 h-20 rounded-full"
/>
)}
<div>
{isEditing ? (
<div className="flex space-x-2">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="flex-1 px-3 py-2 border rounded"
/>
<button
onClick={handleUpdate}
className="px-3 py-2 bg-green-500 text-white rounded"
>
Save
</button>
<button
onClick={() => setIsEditing(false)}
className="px-3 py-2 bg-gray-500 text-white rounded"
>
Cancel
</button>
</div>
) : (
<div>
<p className="font-semibold">{user?.name}</p>
<p className="text-gray-600">{user?.email}</p>
<p className="text-sm text-gray-500">Role: {user?.role}</p>
<button
onClick={() => {
setEditName(user?.name || '')
setIsEditing(true)
}}
className="mt-2 px-3 py-1 bg-blue-500 text-white rounded text-sm"
>
Edit Profile
</button>
</div>
)}
</div>
<button
onClick={logout}
className="px-4 py-2 bg-red-500 text-white rounded"
>
Logout
</button>
</div>
</div>
)
}
💻 Patrones Avanzados de Zustand typescript
🟡 intermediate
⭐⭐⭐⭐
Patrones complejos de Zustand incluyendo middleware, persistencia, selectores y optimización de rendimiento
⏱️ 40 min
🏷️ zustand, advanced, state-management, middleware
Prerequisites:
Zustand basics, React hooks, TypeScript, Testing fundamentals
// Advanced Zustand Patterns
// 1. Persistence with Middleware
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface UserPreferences {
theme: 'light' | 'dark'
language: string
notifications: boolean
fontSize: 'small' | 'medium' | 'large'
}
const usePreferencesStore = create<UserPreferences>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
notifications: true,
fontSize: 'medium',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleNotifications: () => set((state) => ({
notifications: !state.notifications
})),
setFontSize: (fontSize) => set({ fontSize }),
}),
{
name: 'user-preferences',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
theme: state.theme,
language: state.language,
fontSize: state.fontSize,
}),
}
)
)
// 2. DevTools Middleware
import { devtools } from 'zustand/middleware'
interface AppStore {
users: User[]
posts: Post[]
addUser: (user: User) => void
addPost: (post: Post) => void
removeUser: (id: string) => void
removePost: (id: string) => void
}
const useAppStore = create<AppStore>()(
devtools(
(set) => ({
users: [],
posts: [],
addUser: (user) => set((state) => ({
users: [...state.users, user]
})),
addPost: (post) => set((state) => ({
posts: [...state.posts, post]
})),
removeUser: (id) => set((state) => ({
users: state.users.filter(user => user.id !== id)
})),
removePost: (id) => set((state) => ({
posts: state.posts.filter(post => post.id !== id)
})),
}),
{
name: 'app-store',
anonymousActionType: 'unknown',
}
)
)
// 3. Subscribe to Store Changes
const useLogger = () => {
const subscribe = useAppStore.subscribe
useEffect(() => {
const unsubscribe = subscribe((state) => {
console.log('Store updated:', state)
})
return unsubscribe
}, [subscribe])
}
// Select specific state changes
useAppStore.subscribe(
(state) => state.users,
(users) => {
console.log('Users changed:', users)
}
)
// 4. Combining Multiple Stores
const useCombinedStore = () => {
const preferences = usePreferencesStore()
const cart = useCartStore()
const getLocalizedPrice = (price: number) => {
const formatter = new Intl.NumberFormat(preferences.language, {
style: 'currency',
currency: preferences.language === 'en' ? 'USD' : 'EUR',
})
return formatter.format(price)
}
return {
...preferences,
...cart,
getLocalizedPrice,
}
}
// 5. Async Actions with Error Handling
interface AsyncStore {
data: any[]
loading: boolean
error: string | null
fetchData: () => Promise<void>
createItem: (item: any) => Promise<void>
updateItem: (id: string, updates: any) => Promise<void>
deleteItem: (id: string) => Promise<void>
clearError: () => void
}
const useAsyncStore = create<AsyncStore>((set, get) => ({
data: [],
loading: false,
error: null,
clearError: () => set({ error: null }),
fetchData: async () => {
set({ loading: true, error: null })
try {
const response = await fetch('/api/data')
const data = await response.json()
set({ data, loading: false })
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to fetch data',
loading: false
})
}
},
createItem: async (item) => {
set({ loading: true, error: null })
try {
const response = await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
})
const newItem = await response.json()
set((state) => ({
data: [...state.data, newItem],
loading: false
}))
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to create item',
loading: false
})
}
},
updateItem: async (id, updates) => {
set({ loading: true, error: null })
try {
const response = await fetch(`/api/data/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
})
const updatedItem = await response.json()
set((state) => ({
data: state.data.map(item =>
item.id === id ? { ...item, ...updatedItem } : item
),
loading: false
}))
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to update item',
loading: false
})
}
},
deleteItem: async (id) => {
set({ loading: true, error: null })
try {
await fetch(`/api/data/${id}`, { method: 'DELETE' })
set((state) => ({
data: state.data.filter(item => item.id !== id),
loading: false
}))
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to delete item',
loading: false
})
}
},
}))
// 6. Custom Selectors for Performance
const useGameStore = create<GameStore>()(
devtools(
persist(
(set, get) => ({
players: [],
enemies: [],
items: [],
score: 0,
level: 1,
health: 100,
// Actions
addPlayer: (player) => set((state) => ({
players: [...state.players, player]
})),
spawnEnemy: (enemy) => set((state) => ({
enemies: [...state.enemies, enemy]
})),
collectItem: (item) => set((state) => ({
items: [...state.items, item],
score: state.score + item.points
})),
takeDamage: (damage) => set((state) => ({
health: Math.max(0, state.health - damage)
})),
levelUp: () => set((state) => ({
level: state.level + 1,
health: state.health + 20
})),
}),
{
name: 'game-store',
storage: createJSONStorage(() => localStorage),
}
)
)
)
// Custom selectors with memoization
const useGameSelectors = () => {
const store = useGameStore()
const aliveEnemies = useStore(useCallback(
(state) => state.enemies.filter(enemy => enemy.health > 0),
[]
))
const playerHealth = useStore(useCallback(
(state) => state.health,
[]
))
const isGameOver = useStore(useCallback(
(state) => state.health <= 0,
[]
))
const highScore = useStore(useCallback(
(state) => state.score,
[]
))
return {
aliveEnemies,
playerHealth,
isGameOver,
highScore,
...store
}
}
// 7. Optimistic Updates
interface OptimisticStore {
posts: Post[]
updatePostOptimistic: (id: string, updates: Partial<Post>) => Promise<void>
deletePostOptimistic: (id: string) => Promise<void>
}
const useOptimisticStore = create<OptimisticStore>((set, get) => ({
posts: [],
updatePostOptimistic: async (id, updates) => {
const previousPosts = get().posts
// Optimistic update
set((state) => ({
posts: state.posts.map(post =>
post.id === id ? { ...post, ...updates } : post
)
}))
try {
const response = await fetch(`/api/posts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
})
if (!response.ok) {
throw new Error('Update failed')
}
const updatedPost = await response.json()
// Confirm update
set((state) => ({
posts: state.posts.map(post =>
post.id === id ? updatedPost : post
)
}))
} catch (error) {
// Revert on error
set({ posts: previousPosts })
throw error
}
},
deletePostOptimistic: async (id) => {
const previousPosts = get().posts
// Optimistic deletion
set((state) => ({
posts: state.posts.filter(post => post.id !== id)
}))
try {
const response = await fetch(`/api/posts/${id}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error('Delete failed')
}
} catch (error) {
// Revert on error
set({ posts: previousPosts })
throw error
}
},
}))
// 8. Testing Zustand Stores
import { act, renderHook } from '@testing-library/react'
import { create } from 'zustand'
// Test store
interface TestStore {
count: number
increment: () => void
decrement: () => void
}
const useTestStore = create<TestStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// Test cases
describe('useTestStore', () => {
it('should initialize with count 0', () => {
const { result } = renderHook(() => useTestStore())
expect(result.current.count).toBe(0)
})
it('should increment count', () => {
const { result } = renderHook(() => useTestStore())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
it('should decrement count', () => {
const { result } = renderHook(() => useTestStore())
act(() => {
result.current.decrement()
})
expect(result.current.count).toBe(-1)
})
})
// 9. TypeScript Best Practices
interface StrictStore {
readonly state: {
user: User | null
status: 'idle' | 'loading' | 'success' | 'error'
}
actions: {
setUser: (user: User) => void
clearUser: () => void
setStatus: (status: StrictStore['state']['status']) => void
}
}
const useStrictStore = create<StrictStore>((set) => ({
state: {
user: null,
status: 'idle' as const,
},
actions: {
setUser: (user) =>
set((state) => ({
state: { ...state.state, user }
})),
clearUser: () =>
set((state) => ({
state: { ...state.state, user: null }
})),
setStatus: (status) =>
set((state) => ({
state: { ...state.state, status }
})),
},
}))
// Usage with destructuring
const { state: { user, status }, actions } = useStrictStore()
// 10. Server-Side Rendering Support
import { create } from 'zustand'
import { createContext, useContext, useRef } from 'react'
// Store interface
interface Store {
count: number
increment: () => void
}
// Create store
const createStore = () => create<Store>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
// Context for store
const StoreContext = createContext<ReturnType<typeof createStore> | null>(null)
// Provider component
export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
const storeRef = useRef<ReturnType<typeof createStore>>()
if (!storeRef.current) {
storeRef.current = createStore()
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
)
}
// Hook to use store
export const useStore = <T>(selector: (store: Store) => T): T => {
const store = useContext(StoreContext)
if (!store) {
throw new Error('useStore must be used within StoreProvider')
}
return useStore(store, selector)
}