🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Zustand
Exemples de la bibliothèque de gestion d'état Zustand incluant la configuration de base, middleware, persistance et patterns avancés
💻 Store de Base Zustand typescript
🟢 simple
⭐⭐
Patterns fondamentaux de store Zustand incluant créer, mettre à jour et consommer l'état dans les composants 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>
)
}
💻 Patterns Avancés Zustand typescript
🟡 intermediate
⭐⭐⭐⭐
Patterns complexes Zustand incluant middleware, persistance, sélecteurs et optimisation des performances
⏱️ 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)
}