Explore Zustand as a simpler, lightweight alternative to Redux for React state management.
Explore Zustand as a simpler, lightweight alternative to Redux for React state management.
Zustand is a small, fast, and scalable state management solution for React. It provides a simplified API compared to Redux while maintaining the benefits of centralized state management.
1// Redux Toolkit approach
2import { configureStore, createSlice } from '@reduxjs/toolkit';
3
4const counterSlice = createSlice({
5 name: 'counter',
6 initialState: { value: 0 },
7 reducers: {
8 increment: (state) => { state.value += 1; },
9 decrement: (state) => { state.value -= 1; }
10 }
11});
12
13const store = configureStore({ reducer: { counter: counterSlice.reducer } });
14
15// Zustand approach
16import { create } from 'zustand';
17
18const useCounterStore = create((set) => ({
19 count: 0,
20 increment: () => set((state) => ({ count: state.count + 1 })),
21 decrement: () => set((state) => ({ count: state.count - 1 }))
22}));1// No providers needed - use directly in components
2function Counter() {
3 const { count, increment, decrement } = useCounterStore();
4
5 return (
6 <div>
7 <span>{count}</span>
8 <button onClick={increment}>+</button>
9 <button onClick={decrement}>-</button>
10 </div>
11 );
12}1const useStore = create((set, get) => ({
2 // State
3 count: 0,
4 name: 'John',
5
6 // Actions
7 increment: () => set((state) => ({ count: state.count + 1 })),
8 reset: () => set({ count: 0 }),
9
10 // Computed values
11 doubled: () => get().count * 2,
12
13 // Complex updates
14 updateProfile: (newData) => set((state) => ({
15 ...state,
16 profile: { ...state.profile, ...newData }
17 }))
18}));1// Subscribe to entire store
2const { count, increment } = useStore();
3
4// Subscribe to specific slice
5const count = useStore((state) => state.count);
6const increment = useStore((state) => state.increment);
7
8// Conditional subscriptions
9const isEven = useStore((state) => state.count % 2 === 0);1// Access store outside React components
2const currentCount = useStore.getState().count;
3useStore.getState().increment();
4
5// Subscribe to changes
6const unsubscribe = useStore.subscribe(
7 (state) => console.log('Count changed:', state.count)
8);1const useAppStore = create((set, get) => ({
2 // User state
3 user: null,
4 isAuthenticated: false,
5
6 // UI state
7 theme: 'light',
8 sidebar: { open: false, collapsed: false },
9
10 // Data state
11 posts: [],
12 loading: false,
13 error: null,
14
15 // User actions
16 login: (userData) => set({
17 user: userData,
18 isAuthenticated: true
19 }),
20
21 logout: () => set({
22 user: null,
23 isAuthenticated: false
24 }),
25
26 // UI actions
27 toggleTheme: () => set((state) => ({
28 theme: state.theme === 'light' ? 'dark' : 'light'
29 })),
30
31 setSidebarOpen: (open) => set((state) => ({
32 sidebar: { ...state.sidebar, open }
33 })),
34
35 // Data actions
36 fetchPosts: async () => {
37 set({ loading: true, error: null });
38 try {
39 const posts = await api.getPosts();
40 set({ posts, loading: false });
41 } catch (error) {
42 set({ error: error.message, loading: false });
43 }
44 }
45}));1// User store
2const useUserStore = create((set) => ({
3 user: null,
4 isAuthenticated: false,
5 login: (userData) => set({ user: userData, isAuthenticated: true }),
6 logout: () => set({ user: null, isAuthenticated: false })
7}));
8
9// UI store
10const useUIStore = create((set) => ({
11 theme: 'light',
12 sidebar: { open: false },
13 toggleTheme: () => set((state) => ({
14 theme: state.theme === 'light' ? 'dark' : 'light'
15 })),
16 setSidebarOpen: (open) => set((state) => ({
17 sidebar: { ...state.sidebar, open }
18 }))
19}));
20
21// Posts store
22const usePostsStore = create((set, get) => ({
23 posts: [],
24 loading: false,
25 error: null,
26
27 fetchPosts: async () => {
28 set({ loading: true, error: null });
29 try {
30 const posts = await api.getPosts();
31 set({ posts, loading: false });
32 } catch (error) {
33 set({ error: error.message, loading: false });
34 }
35 },
36
37 addPost: (post) => set((state) => ({
38 posts: [post, ...state.posts]
39 })),
40
41 updatePost: (id, updates) => set((state) => ({
42 posts: state.posts.map(post =>
43 post.id === id ? { ...post, ...updates } : post
44 )
45 }))
46}));1// Create reusable store slices
2const createUserSlice = (set, get) => ({
3 user: null,
4 isAuthenticated: false,
5
6 login: (userData) => set((state) => ({
7 ...state,
8 user: userData,
9 isAuthenticated: true
10 })),
11
12 logout: () => set((state) => ({
13 ...state,
14 user: null,
15 isAuthenticated: false
16 }))
17});
18
19const createUISlice = (set, get) => ({
20 theme: 'light',
21 loading: false,
22
23 setTheme: (theme) => set((state) => ({ ...state, theme })),
24 setLoading: (loading) => set((state) => ({ ...state, loading }))
25});
26
27// Combine slices
28const useAppStore = create((set, get) => ({
29 ...createUserSlice(set, get),
30 ...createUISlice(set, get),
31
32 // Cross-slice actions
33 initializeApp: async () => {
34 get().setLoading(true);
35 try {
36 const userData = await api.getCurrentUser();
37 get().login(userData);
38 } finally {
39 get().setLoading(false);
40 }
41 }
42}));1const useStore = create((set, get) => ({
2 todos: [],
3 filter: 'all',
4
5 addTodo: (text) => set((state) => ({
6 todos: [...state.todos, {
7 id: Date.now(),
8 text,
9 completed: false
10 }]
11 })),
12
13 toggleTodo: (id) => set((state) => ({
14 todos: state.todos.map(todo =>
15 todo.id === id ? { ...todo, completed: !todo.completed } : todo
16 )
17 })),
18
19 setFilter: (filter) => set({ filter }),
20
21 // Computed selectors as methods
22 getFilteredTodos: () => {
23 const { todos, filter } = get();
24 switch (filter) {
25 case 'active': return todos.filter(t => !t.completed);
26 case 'completed': return todos.filter(t => t.completed);
27 default: return todos;
28 }
29 },
30
31 getStats: () => {
32 const todos = get().todos;
33 return {
34 total: todos.length,
35 completed: todos.filter(t => t.completed).length,
36 active: todos.filter(t => !t.completed).length
37 };
38 }
39}));
40
41// Usage with computed values
42function TodoStats() {
43 const getStats = useStore((state) => state.getStats);
44 const stats = getStats();
45
46 return (
47 <div>
48 <span>Total: {stats.total}</span>
49 <span>Active: {stats.active}</span>
50 <span>Completed: {stats.completed}</span>
51 </div>
52 );
53}1import { subscribeWithSelector } from 'zustand/middleware';
2
3const useStore = create(
4 subscribeWithSelector(
5 (set, get) => ({
6 count: 0,
7 increment: () => set((state) => ({ count: state.count + 1 }))
8 })
9 )
10);
11
12// Subscribe to specific state changes
13useStore.subscribe(
14 (state) => state.count,
15 (count, prevCount) => {
16 console.log('Count changed from', prevCount, 'to', count);
17 }
18);1const useDataStore = create((set, get) => ({
2 data: null,
3 loading: false,
4 error: null,
5
6 fetchData: async (id) => {
7 set({ loading: true, error: null });
8
9 try {
10 const response = await fetch(`/api/data/${id}`);
11 if (!response.ok) throw new Error('Failed to fetch');
12
13 const data = await response.json();
14 set({ data, loading: false });
15 } catch (error) {
16 set({ error: error.message, loading: false });
17 }
18 },
19
20 createData: async (newData) => {
21 set({ loading: true, error: null });
22
23 try {
24 const response = await fetch('/api/data', {
25 method: 'POST',
26 headers: { 'Content-Type': 'application/json' },
27 body: JSON.stringify(newData)
28 });
29
30 if (!response.ok) throw new Error('Failed to create');
31
32 const created = await response.json();
33 set((state) => ({
34 data: [...(state.data || []), created],
35 loading: false
36 }));
37 } catch (error) {
38 set({ error: error.message, loading: false });
39 }
40 }
41}));1const useSearchStore = create((set, get) => ({
2 query: '',
3 results: [],
4 loading: false,
5 error: null,
6 abortController: null,
7
8 setQuery: (query) => {
9 set({ query });
10 get().search(query);
11 },
12
13 search: async (query) => {
14 // Cancel previous request
15 const prevController = get().abortController;
16 if (prevController) {
17 prevController.abort();
18 }
19
20 const abortController = new AbortController();
21 set({ loading: true, error: null, abortController });
22
23 try {
24 const response = await fetch(`/api/search?q=${query}`, {
25 signal: abortController.signal
26 });
27
28 if (!response.ok) throw new Error('Search failed');
29
30 const results = await response.json();
31 set({ results, loading: false, abortController: null });
32 } catch (error) {
33 if (error.name === 'AbortError') {
34 console.log('Search cancelled');
35 } else {
36 set({ error: error.message, loading: false });
37 }
38 }
39 },
40
41 clearSearch: () => {
42 const controller = get().abortController;
43 if (controller) controller.abort();
44
45 set({
46 query: '',
47 results: [],
48 loading: false,
49 error: null,
50 abortController: null
51 });
52 }
53}));1const usePostsStore = create((set, get) => ({
2 posts: [],
3 optimisticUpdates: new Set(),
4
5 addPostOptimistic: async (postData) => {
6 const tempId = `temp-${Date.now()}`;
7 const optimisticPost = {
8 id: tempId,
9 ...postData,
10 createdAt: new Date().toISOString(),
11 isOptimistic: true
12 };
13
14 // Add optimistic post
15 set((state) => ({
16 posts: [optimisticPost, ...state.posts],
17 optimisticUpdates: new Set([...state.optimisticUpdates, tempId])
18 }));
19
20 try {
21 const response = await fetch('/api/posts', {
22 method: 'POST',
23 headers: { 'Content-Type': 'application/json' },
24 body: JSON.stringify(postData)
25 });
26
27 if (!response.ok) throw new Error('Failed to create post');
28
29 const realPost = await response.json();
30
31 // Replace optimistic post with real post
32 set((state) => {
33 const updatedOptimistic = new Set(state.optimisticUpdates);
34 updatedOptimistic.delete(tempId);
35
36 return {
37 posts: state.posts.map(post =>
38 post.id === tempId ? realPost : post
39 ),
40 optimisticUpdates: updatedOptimistic
41 };
42 });
43 } catch (error) {
44 // Remove failed optimistic post
45 set((state) => {
46 const updatedOptimistic = new Set(state.optimisticUpdates);
47 updatedOptimistic.delete(tempId);
48
49 return {
50 posts: state.posts.filter(post => post.id !== tempId),
51 optimisticUpdates: updatedOptimistic
52 };
53 });
54
55 throw error;
56 }
57 },
58
59 updatePostOptimistic: async (id, updates) => {
60 const originalPost = get().posts.find(p => p.id === id);
61 if (!originalPost) return;
62
63 // Apply optimistic update
64 set((state) => ({
65 posts: state.posts.map(post =>
66 post.id === id ? { ...post, ...updates } : post
67 ),
68 optimisticUpdates: new Set([...state.optimisticUpdates, id])
69 }));
70
71 try {
72 const response = await fetch(`/api/posts/${id}`, {
73 method: 'PATCH',
74 headers: { 'Content-Type': 'application/json' },
75 body: JSON.stringify(updates)
76 });
77
78 if (!response.ok) throw new Error('Failed to update post');
79
80 const updatedPost = await response.json();
81
82 // Confirm update
83 set((state) => {
84 const updatedOptimistic = new Set(state.optimisticUpdates);
85 updatedOptimistic.delete(id);
86
87 return {
88 posts: state.posts.map(post =>
89 post.id === id ? updatedPost : post
90 ),
91 optimisticUpdates: updatedOptimistic
92 };
93 });
94 } catch (error) {
95 // Revert optimistic update
96 set((state) => {
97 const updatedOptimistic = new Set(state.optimisticUpdates);
98 updatedOptimistic.delete(id);
99
100 return {
101 posts: state.posts.map(post =>
102 post.id === id ? originalPost : post
103 ),
104 optimisticUpdates: updatedOptimistic
105 };
106 });
107
108 throw error;
109 }
110 }
111}));1import { persist } from 'zustand/middleware';
2
3const useSettingsStore = create(
4 persist(
5 (set) => ({
6 theme: 'light',
7 language: 'en',
8 notifications: true,
9
10 setTheme: (theme) => set({ theme }),
11 setLanguage: (language) => set({ language }),
12 toggleNotifications: () => set((state) => ({
13 notifications: !state.notifications
14 }))
15 }),
16 {
17 name: 'app-settings', // Storage key
18 partialize: (state) => ({ // Only persist certain fields
19 theme: state.theme,
20 language: state.language
21 })
22 }
23 )
24);1interface User {
2 id: string;
3 name: string;
4 email: string;
5 role: 'admin' | 'user';
6}
7
8interface UserState {
9 user: User | null;
10 users: User[];
11 loading: boolean;
12 error: string | null;
13}
14
15interface UserActions {
16 login: (user: User) => void;
17 logout: () => void;
18 fetchUsers: () => Promise<void>;
19 updateUser: (id: string, updates: Partial<User>) => Promise<void>;
20 setError: (error: string | null) => void;
21}
22
23type UserStore = UserState & UserActions;
24
25const useUserStore = create<UserStore>()((set, get) => ({
26 // State
27 user: null,
28 users: [],
29 loading: false,
30 error: null,
31
32 // Actions
33 login: (user) => set({ user }),
34
35 logout: () => set({ user: null }),
36
37 fetchUsers: async () => {
38 set({ loading: true, error: null });
39 try {
40 const response = await fetch('/api/users');
41 if (!response.ok) throw new Error('Failed to fetch users');
42
43 const users = await response.json();
44 set({ users, loading: false });
45 } catch (error) {
46 set({
47 error: error instanceof Error ? error.message : 'Unknown error',
48 loading: false
49 });
50 }
51 },
52
53 updateUser: async (id, updates) => {
54 const currentUsers = get().users;
55
56 // Optimistic update
57 set({
58 users: currentUsers.map(user =>
59 user.id === id ? { ...user, ...updates } : user
60 )
61 });
62
63 try {
64 const response = await fetch(`/api/users/${id}`, {
65 method: 'PATCH',
66 headers: { 'Content-Type': 'application/json' },
67 body: JSON.stringify(updates)
68 });
69
70 if (!response.ok) throw new Error('Failed to update user');
71
72 const updatedUser = await response.json();
73 set({
74 users: currentUsers.map(user =>
75 user.id === id ? updatedUser : user
76 )
77 });
78 } catch (error) {
79 // Revert on failure
80 set({ users: currentUsers });
81 set({
82 error: error instanceof Error ? error.message : 'Update failed'
83 });
84 }
85 },
86
87 setError: (error) => set({ error })
88}));1// Define slice types
2interface AuthSlice {
3 isAuthenticated: boolean;
4 user: User | null;
5 login: (user: User) => void;
6 logout: () => void;
7}
8
9interface UISlice {
10 theme: 'light' | 'dark';
11 sidebarOpen: boolean;
12 setTheme: (theme: 'light' | 'dark') => void;
13 setSidebarOpen: (open: boolean) => void;
14}
15
16// Create typed slice creators
17const createAuthSlice = (
18 set: (fn: (state: AppStore) => AppStore) => void,
19 get: () => AppStore
20): AuthSlice => ({
21 isAuthenticated: false,
22 user: null,
23
24 login: (user) => set((state) => ({
25 ...state,
26 user,
27 isAuthenticated: true
28 })),
29
30 logout: () => set((state) => ({
31 ...state,
32 user: null,
33 isAuthenticated: false
34 }))
35});
36
37const createUISlice = (
38 set: (fn: (state: AppStore) => AppStore) => void,
39 get: () => AppStore
40): UISlice => ({
41 theme: 'light',
42 sidebarOpen: false,
43
44 setTheme: (theme) => set((state) => ({ ...state, theme })),
45 setSidebarOpen: (sidebarOpen) => set((state) => ({ ...state, sidebarOpen }))
46});
47
48// Combine slices
49type AppStore = AuthSlice & UISlice;
50
51const useAppStore = create<AppStore>()((set, get) => ({
52 ...createAuthSlice(set, get),
53 ...createUISlice(set, get)
54}));1// Strongly typed selectors
2type StoreSelector<T> = (state: AppStore) => T;
3
4const selectUser: StoreSelector<User | null> = (state) => state.user;
5const selectIsAdmin: StoreSelector<boolean> = (state) =>
6 state.user?.role === 'admin' ?? false;
7const selectUserName: StoreSelector<string> = (state) =>
8 state.user?.name ?? 'Guest';
9
10// Usage with type safety
11function UserProfile() {
12 const user = useAppStore(selectUser);
13 const isAdmin = useAppStore(selectIsAdmin);
14 const userName = useAppStore(selectUserName);
15
16 // TypeScript knows the exact types
17 return (
18 <div>
19 <h1>Welcome, {userName}</h1>
20 {user && <p>Email: {user.email}</p>}
21 {isAdmin && <button>Admin Panel</button>}
22 </div>
23 );
24}
25
26// Generic store creation helper
27function createTypedStore<T>(initializer: (set: any, get: any) => T) {
28 return create<T>()(initializer);
29}
30
31// Usage
32interface CounterStore {
33 count: number;
34 increment: () => void;
35 decrement: () => void;
36 reset: () => void;
37}
38
39const useCounterStore = createTypedStore<CounterStore>((set) => ({
40 count: 0,
41 increment: () => set((state) => ({ count: state.count + 1 })),
42 decrement: () => set((state) => ({ count: state.count - 1 })),
43 reset: () => set({ count: 0 })
44}));1// ✅ Good - Pure action
2const useStore = create<Store>((set, get) => ({
3 items: [],
4 addItem: (item: Item) => set((state) => ({
5 items: [...state.items, { ...item, id: generateId() }]
6 }))
7}));
8
9// ❌ Bad - Side effects in action
10const useStore = create<Store>((set, get) => ({
11 items: [],
12 addItem: (item: Item) => {
13 // Side effect - should be in component or effect
14 analytics.track('item_added', item);
15
16 set((state) => ({
17 items: [...state.items, { ...item, id: generateId() }]
18 }));
19 }
20}));1import { shallow } from 'zustand/shallow';
2
3// When selecting multiple values
4function MyComponent() {
5 const { user, theme } = useStore(
6 (state) => ({ user: state.user, theme: state.theme }),
7 shallow
8 );
9
10 // Component only re-renders when user or theme actually change
11}1// Split large stores into logical slices
2const useStore = create<AppStore>()((set, get) => ({
3 // Auth slice
4 ...createAuthSlice(set, get),
5
6 // UI slice
7 ...createUISlice(set, get),
8
9 // Data slice
10 ...createDataSlice(set, get),
11
12 // Cross-slice actions
13 initialize: async () => {
14 const { fetchUser, setLoading } = get();
15 setLoading(true);
16 try {
17 await fetchUser();
18 } finally {
19 setLoading(false);
20 }
21 }
22}));1interface AsyncState<T> {
2 data: T | null;
3 loading: boolean;
4 error: string | null;
5 lastUpdated: number | null;
6}
7
8function createAsyncSlice<T>(name: string, fetchFn: () => Promise<T>) {
9 return (set: any, get: any) => ({
10 [`${name}Data`]: null as T | null,
11 [`${name}Loading`]: false,
12 [`${name}Error`]: null as string | null,
13 [`${name}LastUpdated`]: null as number | null,
14
15 [`fetch${name}`]: async () => {
16 set({ [`${name}Loading`]: true, [`${name}Error`]: null });
17 try {
18 const data = await fetchFn();
19 set({
20 [`${name}Data`]: data,
21 [`${name}Loading`]: false,
22 [`${name}LastUpdated`]: Date.now()
23 });
24 } catch (error) {
25 set({
26 [`${name}Error`]: error.message,
27 [`${name}Loading`]: false
28 });
29 }
30 }
31 });
32}Your progress is saved in this browser, on this device only.