We use cookies to enhance your experience on the site
CodeWorlds

Next.js z Zustand - Nowoczesne state management

In Quantum Metropolis, where application state is managed with quantum precision, you will discover Zustand - a minimalist yet powerful state management library. Combined with Next.js it creates an elegant solution for building scalable applications with a clean data flow.

What is Zustand?

Zustand is a modern state management library that offers:

  • Minimalist API - simple and intuitive API
  • TypeScript-first - full type support
  • No boilerplate - no boilerplate code
  • SSR-friendly - support for Server-Side Rendering
  • DevTools support - integration with Redux DevTools
  • Middleware ecosystem - a rich middleware ecosystem

Advantages of Zustand over Redux

  • Less code - no actions, reducers or dispatchers
  • Better TypeScript - natural type support
  • Modular design - easy store splitting
  • Performance - automatic re-render optimization

Installation and basic configuration

Installation

1# Install Zustand
2npm install zustand
3
4# For development tools (optional)
5npm install @redux-devtools/extension

Basic store

1// stores/counterStore.ts
2import { create } from 'zustand';
3
4interface CounterState {
5  count: number;
6  increment: () => void;
7  decrement: () => void;
8  reset: () => void;
9}
10
11export const useCounterStore = create<CounterState>()((set) => ({
12  count: 0,
13  increment: () => set((state) => ({ count: state.count + 1 })),
14  decrement: () => set((state) => ({ count: state.count - 1 })),
15  reset: () => set({ count: 0 }),
16}));

Usage in a component

1// components/Counter.tsx
2'use client';
3
4import { useCounterStore } from '@/stores/counterStore';
5
6export default function Counter() {
7  const { count, andncrement, decrement, reset } = useCounterStore();
8
9  return (
10    <div className="bg-white p-6 rounded-lg shadow">
11      <h2 className="text-2xl font-bold mb-4">Counter: {count}</h2>
12      
13      <div className="space-x-4">
14        <button
15          onClick={increment}
16          className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
17        >
18          +1
19        </button>
20        
21        <button
22          onClick={decrement}
23          className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
24        >
25          -1
26        </button>
27        
28        <button
29          onClick={reset}
30          className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
31        >
32          Reset
33        </button>
34      </div>
35    </div>
36  );
37}

Advanced patterns

User store with authentication

1// stores/userStore.ts
2import { create } from 'zustand';
3import { persist, createJSONStorage } from 'zustand/middleware';
4
5interface User {
6  id: string;
7  name: string;
8  email: string;
9  avatar?: string;
10  role: 'user' | 'admin';
11}
12
13interface UserState {
14  user: User | null;
15  isLoading: boolean;
16  error: string | null;
17  login: (email: string, password: string) => Promise<void>;
18  logout: () => void;
19  updateProfile: (data: Partial<User>) => Promise<void>;
20  setUser: (user: User) => void;
21  clearError: () => void;
22}
23
24export const useUserStore = create<UserState>()(
25  persist(
26    (set, get) => ({
27      user: null,
28      isLoading: false,
29      error: null,
30
31      login: async (email: string, password: string) => {
32        set({ isLoading: true, error: null });
33        
34        try {
35          const response = await fetch('/api/auth/login', {
36            method: 'POST',
37            headers: { 'Content-Type': 'application/json' },
38            body: JSON.stringify({ email, password }),
39          });
40
41          if (!response.ok) {
42            throw new Error('Invalid login credentials');
43          }
44
45          const userData = await response.json();
46          set({ user: userData.user, andsLoading: false });
47        } catch (error) {
48          set({ 
49            error: error instanceof Error ? error.message : 'An error occurred',
50            isLoading: false 
51          });
52        }
53      },
54
55      logout: () => {
56        set({ user: null, error: null });
57        // Call logout API
58        fetch('/api/auth/logout', { method: 'POST' });
59      },
60
61      updateProfile: async (data: Partial<User>) => {
62        const { user } = get();
63        if (!user) return;
64
65        set({ isLoading: true });
66
67        try {
68          const response = await fetch('/api/user/profile', {
69            method: 'PATCH',
70            headers: { 'Content-Type': 'application/json' },
71            body: JSON.stringify(data),
72          });
73
74          if (!response.ok) {
75            throw new Error('Failed to update profile');
76          }
77
78          const updatedUser = await response.json();
79          set({ user: updatedUser, andsLoading: false });
80        } catch (error) {
81          set({ 
82            error: error instanceof Error ? error.message : 'An error occurred',
83            isLoading: false 
84          });
85        }
86      },
87
88      setUser: (user: User) => set({ user }),
89      clearError: () => set({ error: null }),
90    }),
91    {
92      name: 'user-storage',
93      storage: createJSONStorage(() => localStorage),
94      partialize: (state) => ({ user: state.user }), // Persist only user data
95    }
96  )
97);

Shopping cart store

1// stores/cartStore.ts
2import { create } from 'zustand';
3import { persist } from 'zustand/middleware';
4
5interface CartItem {
6  id: string;
7  name: string;
8  price: number;
9  quantity: number;
10  image?: string;
11  variant?: {
12    size?: string;
13    color?: string;
14  };
15}
16
17interface CartState {
18  items: CartItem[];
19  isOpen: boolean;
20  addItem: (item: Omit<CartItem, 'quantity'>) => void;
21  removeItem: (id: string) => void;
22  updateQuantity: (id: string, quantity: number) => void;
23  clearCart: () => void;
24  toggleCart: () => void;
25  getTotalItems: () => number;
26  getTotalPrice: () => number;
27}
28
29export const useCartStore = create<CartState>()(
30  persist(
31    (set, get) => ({
32      items: [],
33      isOpen: false,
34
35      addItem: (newItem) => {
36        const { items } = get();
37        const existingItem = items.find(item => 
38          item.id === newItem.id && 
39          JSON.stringify(item.variant) === JSON.stringify(newItem.variant)
40        );
41
42        if (existingItem) {
43          set({
44            items: items.map(item =>
45              item === existingItem
46                ? { ...item, quantity: item.quantity + 1 }
47                : item
48            ),
49          });
50        } else {
51          set({
52            items: [...items, { ...newItem, quantity: 1 }],
53          });
54        }
55      },
56
57      removeItem: (id) => {
58        set({
59          items: get().items.filter(item => item.id !== id),
60        });
61      },
62
63      updateQuantity: (id, quantity) => {
64        if (quantity <= 0) {
65          get().removeItem(id);
66          return;
67        }
68
69        set({
70          items: get().items.map(item =>
71            item.id === id ? { ...item, quantity } : item
72          ),
73        });
74      },
75
76      clearCart: () => set({ items: [] }),
77
78      toggleCart: () => set({ isOpen: !get().isOpen }),
79
80      getTotalItems: () => {
81        return get().items.reduce((total, andtem) => total + item.quantity, 0);
82      },
83
84      getTotalPrice: () => {
85        return get().items.reduce(
86          (total, andtem) => total + item.price * item.quantity,
87          0
88        );
89      },
90    }),
91    {
92      name: 'cart-storage',
93    }
94  )
95);

Middleware i DevTools

Redux DevTools integration

1// stores/appStore.ts
2import { create } from 'zustand';
3import { devtools } from 'zustand/middleware';
4
5interface AppState {
6  theme: 'light' | 'dark';
7  sidebarOpen: boolean;
8  notifications: Array<{
9    id: string;
10    type: 'info' | 'success' | 'warning' | 'error';
11    message: string;
12    timestamp: number;
13  }>;
14  toggleTheme: () => void;
15  toggleSidebar: () => void;
16  addNotification: (notification: Omit<AppState['notifications'][0], 'id' | 'timestamp'>) => void;
17  removeNotification: (id: string) => void;
18}
19
20export const useAppStore = create<AppState>()(
21  devtools(
22    (set, get) => ({
23      theme: 'light',
24      sidebarOpen: false,
25      notifications: [],
26
27      toggleTheme: () => {
28        const newTheme = get().theme === 'light' ? 'dark' : 'light';
29        set({ theme: newTheme });
30        
31        // Update document class
32        if (typeof window !== 'undefined') {
33          document.documentElement.classList.toggle('dark', newTheme === 'dark');
34        }
35      },
36
37      toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
38
39      addNotification: (notification) => {
40        const id = Math.random().toString(36).substring(2);
41        const timestamp = Date.now();
42        
43        set({
44          notifications: [
45            ...get().notifications,
46            { ...notification, andd, timestamp }
47          ]
48        });
49
50        // Auto remove after 5 seconds
51        setTimeout(() => {
52          get().removeNotification(id);
53        }, 5000);
54      },
55
56      removeNotification: (id) => {
57        set({
58          notifications: get().notifications.filter(n => n.id !== id)
59        });
60      },
61    }),
62    {
63      name: 'app-store',
64    }
65  )
66);

Immer middleware dla immutable updates

1npm install immer
1// stores/todosStore.ts
2import { create } from 'zustand';
3import { immer } from 'zustand/middleware/immer';
4
5interface Todo {
6  id: string;
7  text: string;
8  completed: boolean;
9  priority: 'low' | 'medium' | 'high';
10  dueDate?: Date;
11}
12
13interface TodosState {
14  todos: Todo[];
15  filter: 'all' | 'active' | 'completed';
16  addTodo: (text: string, priority?: Todo['priority']) => void;
17  toggleTodo: (id: string) => void;
18  deleteTodo: (id: string) => void;
19  editTodo: (id: string, text: string) => void;
20  setFilter: (filter: TodosState['filter']) => void;
21  clearCompleted: () => void;
22}
23
24export const useTodosStore = create<TodosState>()(
25  immer((set) => ({
26    todos: [],
27    filter: 'all',
28
29    addTodo: (text, priority = 'medium') =>
30      set((state) => {
31        state.todos.push({
32          id: Math.random().toString(36).substring(2),
33          text,
34          completed: false,
35          priority,
36        });
37      }),
38
39    toggleTodo: (id) =>
40      set((state) => {
41        const todo = state.todos.find((t) => t.id === id);
42        if (todo) {
43          todo.completed = !todo.completed;
44        }
45      }),
46
47    deleteTodo: (id) =>
48      set((state) => {
49        state.todos = state.todos.filter((t) => t.id !== id);
50      }),
51
52    editTodo: (id, text) =>
53      set((state) => {
54        const todo = state.todos.find((t) => t.id === id);
55        if (todo) {
56          todo.text = text;
57        }
58      }),
59
60    setFilter: (filter) =>
61      set((state) => {
62        state.filter = filter;
63      }),
64
65    clearCompleted: () =>
66      set((state) => {
67        state.todos = state.todos.filter((t) => !t.completed);
68      }),
69  }))
70);

SSR i hydration

Store provider dla SSR

1// providers/StoreProvider.tsx
2'use client';
3
4import { type ReactNode, createContext, useRef, useContext } from 'react';
5import { useStore } from 'zustand';
6import { type UserStore, createUserStore, andnitUserStore } from '@/stores/userStore';
7
8export type UserStoreApi = ReturnType<typeof createUserStore>;
9
10export const UserStoreContext = createContext<UserStoreApi | undefined>(undefined);
11
12export interface UserStoreProviderProps {
13  children: ReactNode;
14  initialState?: Parameters<typeof initUserStore>[0];
15}
16
17export const UserStoreProvider = ({ children, andnitialState }: UserStoreProviderProps) => {
18  const storeRef = useRef<UserStoreApi>();
19  
20  if (!storeRef.current) {
21    storeRef.current = createUserStore(initUserStore(initialState));
22  }
23
24  return (
25    <UserStoreContext.Provider value={storeRef.current}>
26      {children}
27    </UserStoreContext.Provider>
28  );
29};
30
31export const useUserStore = <T,>(selector: (store: UserStore) => T): T => {
32  const userStoreContext = useContext(UserStoreContext);
33
34  if (!userStoreContext) {
35    throw new Error('useUserStore must be used within UserStoreProvider');
36  }
37
38  return useStore(userStoreContext, selector);
39};

Updated store definition

1// stores/userStore.ts (updated for SSR)
2import { createStore } from 'zustand/vanilla';
3
4export interface UserStore {
5  user: User | null;
6  isLoading: boolean;
7  login: (email: string, password: string) => Promise<void>;
8  logout: () => void;
9}
10
11export const initUserStore = (initialState?: Partial<UserStore>): UserStore => {
12  return {
13    user: null,
14    isLoading: false,
15    login: async (email: string, password: string) => {
16      // Implementation
17    },
18    logout: () => {
19      // Implementation
20    },
21    ...initialState,
22  };
23};
24
25export const createUserStore = (initialState: UserStore) => {
26  return createStore<UserStore>()(() => initialState);
27};

Komponenty z Zustand

Theme toggle component

1// components/ThemeToggle.tsx
2'use client';
3
4import { useAppStore } from '@/stores/appStore';
5import { Moon, Sun } from 'lucide-react';
6
7export default function ThemeToggle() {
8  const { theme, toggleTheme } = useAppStore();
9
10  return (
11    <button
12      onClick={toggleTheme}
13      className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
14      aria-label="Toggle theme"
15    >
16      {theme === 'light' ? (
17        <Moon className="w-5 h-5" />
18      ) : (
19        <Sun className="w-5 h-5" />
20      )}
21    </button>
22  );
23}

Shopping cart drawer

1// components/CartDrawer.tsx
2'use client';
3
4import { useCartStore } from '@/stores/cartStore';
5import { X, Plus, Minus, Trash2 } from 'lucide-react';
6
7export default function CartDrawer() {
8  const {
9    items,
10    isOpen,
11    toggleCart,
12    updateQuantity,
13    removeItem,
14    getTotalPrice,
15    getTotalItems,
16  } = useCartStore();
17
18  if (!isOpen) return null;
19
20  return (
21    <div className="fixed inset-0 z-50">
22      {/* Overlay */}
23      <div 
24        className="absolute inset-0 bg-black bg-opacity-50"
25        onClick={toggleCart}
26      />
27      
28      {/* Drawer */}
29      <div className="absolute right-0 top-0 h-full w-96 bg-white shadow-xl">
30        <div className="flex items-center justify-between p-4 border-b">
31          <h2 className="text-lg font-semibold">
32            Koszyk ({getTotalItems()})
33          </h2>
34          <button
35            onClick={toggleCart}
36            className="p-2 hover:bg-gray-100 rounded"
37          >
38            <X className="w-5 h-5" />
39          </button>
40        </div>
41
42        <div className="flex-1 overflow-y-auto p-4">
43          {items.length === 0 ? (
44            <p className="text-gray-500 text-center mt-8">
45              Koszyk jest pusty
46            </p>
47          ) : (
48            <div className="space-y-4">
49              {items.map((item) => (
50                <div key={item.id} className="flex items-center space-x-4 p-4 border rounded">
51                  {item.image && (
52                    <img
53                      src={item.image}
54                      alt={item.name}
55                      className="w-16 h-16 object-cover rounded"
56                    />
57                  )}
58                  
59                  <div className="flex-1">
60                    <h3 className="font-medium">{item.name}</h3>
61                    <p className="text-gray-600">{item.price} USD</p>
62                    
63                    {item.variant && (
64                      <p className="text-sm text-gray-500">
65                        {item.variant.size && `Size: ${item.variant.size}`}
66                        {item.variant.color && ` Color: ${item.variant.color}`}
67                      </p>
68                    )}
69                  </div>
70
71                  <div className="flex items-center space-x-2">
72                    <button
73                      onClick={() => updateQuantity(item.id, andtem.quantity - 1)}
74                      className="p-1 hover:bg-gray-100 rounded"
75                    >
76                      <Minus className="w-4 h-4" />
77                    </button>
78                    
79                    <span className="w-8 text-center">{item.quantity}</span>
80                    
81                    <button
82                      onClick={() => updateQuantity(item.id, andtem.quantity + 1)}
83                      className="p-1 hover:bg-gray-100 rounded"
84                    >
85                      <Plus className="w-4 h-4" />
86                    </button>
87                    
88                    <button
89                      onClick={() => removeItem(item.id)}
90                      className="p-1 hover:bg-red-100 text-red-600 rounded"
91                    >
92                      <Trash2 className="w-4 h-4" />
93                    </button>
94                  </div>
95                </div>
96              ))}
97            </div>
98          )}
99        </div>
100
101        {items.length > 0 && (
102          <div className="border-t p-4">
103            <div className="flex justify-between mb-4">
104              <span className="font-semibold">Total:</span>
105              <span className="font-semibold">{getTotalPrice()} USD</span>
106            </div>
107            
108            <button className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors">
109              Go to checkout
110            </button>
111          </div>
112        )}
113      </div>
114    </div>
115  );
116}

Notifications component

1// components/Notifications.tsx
2'use client';
3
4import { useEffect } from 'react';
5import { useAppStore } from '@/stores/appStore';
6import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
7
8export default function Notifications() {
9  const { notifications, removeNotification } = useAppStore();
10
11  return (
12    <div className="fixed top-4 right-4 z-50 space-y-2">
13      {notifications.map((notification) => (
14        <NotificationItem
15          key={notification.id}
16          notification={notification}
17          onRemove={() => removeNotification(notification.id)}
18        />
19      ))}
20    </div>
21  );
22}
23
24interface NotificationItemProps {
25  notification: {
26    id: string;
27    type: 'info' | 'success' | 'warning' | 'error';
28    message: string;
29    timestamp: number;
30  };
31  onRemove: () => void;
32}
33
34function NotificationItem({ notification, onRemove }: NotificationItemProps) {
35  const { type, message } = notification;
36
37  const icons = {
38    info: Info,
39    success: CheckCircle,
40    warning: AlertTriangle,
41    error: AlertCircle,
42  };
43
44  const colors = {
45    info: 'bg-blue-50 border-blue-200 text-blue-800',
46    success: 'bg-green-50 border-green-200 text-green-800',
47    warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
48    error: 'bg-red-50 border-red-200 text-red-800',
49  };
50
51  const Icon = icons[type];
52
53  useEffect(() => {
54    const timer = setTimeout(onRemove, 5000);
55    return () => clearTimeout(timer);
56  }, [onRemove]);
57
58  return (
59    <div className={`flex items-center p-4 border rounded-lg shadow-lg ${colors[type]} min-w-80 max-w-md`}>
60      <Icon className="w-5 h-5 mr-3 flex-shrink-0" />
61      
62      <div className="flex-1">
63        <p className="text-sm font-medium">{message}</p>
64      </div>
65      
66      <button
67        onClick={onRemove}
68        className="ml-3 flex-shrink-0 p-1 hover:bg-white hover:bg-opacity-20 rounded"
69      >
70        <X className="w-4 h-4" />
71      </button>
72    </div>
73  );
74}

API integration

API client z Zustand

1// lib/api-client.ts
2import { useUserStore } from '@/stores/userStore';
3
4class ApiClient {
5  private baseURL: string;
6
7  constructor(baseURL: string) {
8    this.baseURL = baseURL;
9  }
10
11  private async request<T>(
12    endpoint: string,
13    options: RequestInit = {}
14  ): Promise<T> {
15    const { user } = useUserStore.getState();
16    
17    const config: RequestInit = {
18      headers: {
19        'Content-Type': 'application/json',
20        ...(user && { Authorization: `Bearer ${user.token}` }),
21        ...options.headers,
22      },
23      ...options,
24    };
25
26    const response = await fetch(`${this.baseURL}${endpoint}`, config);
27
28    if (!response.ok) {
29      throw new Error(`API Error: ${response.status}`);
30    }
31
32    return response.json();
33  }
34
35  get<T>(endpoint: string): Promise<T> {
36    return this.request<T>(endpoint);
37  }
38
39  post<T>(endpoint: string, data: any): Promise<T> {
40    return this.request<T>(endpoint, {
41      method: 'POST',
42      body: JSON.stringify(data),
43    });
44  }
45
46  put<T>(endpoint: string, data: any): Promise<T> {
47    return this.request<T>(endpoint, {
48      method: 'PUT',
49      body: JSON.stringify(data),
50    });
51  }
52
53  delete<T>(endpoint: string): Promise<T> {
54    return this.request<T>(endpoint, {
55      method: 'DELETE',
56    });
57  }
58}
59
60export const apiClient = new ApiClient(process.env.NEXT_PUBLIC_API_URL || '');

Products store z API

1// stores/productsStore.ts
2import { create } from 'zustand';
3import { apiClient } from '@/lib/api-client';
4
5interface Product {
6  id: string;
7  name: string;
8  price: number;
9  description: string;
10  images: string[];
11  category: string;
12  inStock: boolean;
13}
14
15interface ProductsState {
16  products: Product[];
17  loading: boolean;
18  error: string | null;
19  searchQuery: string;
20  selectedCategory: string | null;
21  fetchProducts: () => Promise<void>;
22  searchProducts: (query: string) => void;
23  filterByCategory: (category: string | null) => void;
24  getFilteredProducts: () => Product[];
25}
26
27export const useProductsStore = create<ProductsState>()((set, get) => ({
28  products: [],
29  loading: false,
30  error: null,
31  searchQuery: '',
32  selectedCategory: null,
33
34  fetchProducts: async () => {
35    set({ loading: true, error: null });
36    
37    try {
38      const products = await apiClient.get<Product[]>('/products');
39      set({ products, loading: false });
40    } catch (error) {
41      set({
42        error: error instanceof Error ? error.message : 'Failed to fetch products',
43        loading: false,
44      });
45    }
46  },
47
48  searchProducts: (query: string) => {
49    set({ searchQuery: query });
50  },
51
52  filterByCategory: (category: string | null) => {
53    set({ selectedCategory: category });
54  },
55
56  getFilteredProducts: () => {
57    const { products, searchQuery, selectedCategory } = get();
58    
59    return products.filter((product) => {
60      const matchesSearch = searchQuery === '' || 
61        product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
62        product.description.toLowerCase().includes(searchQuery.toLowerCase());
63      
64      const matchesCategory = selectedCategory === null || 
65        product.category === selectedCategory;
66      
67      return matchesSearch && matchesCategory;
68    });
69  },
70}));

Performance optimization

Selectors dla optymalizacji

1// hooks/useOptimizedSelectors.ts
2import { useShallow } from 'zustand/react/shallow';
3import { useCartStore } from '@/stores/cartStore';
4
5// ✅ Good - use selectors
6export const useCartInfo = () => {
7  return useCartStore(
8    useShallow((state) => ({
9      totalItems: state.getTotalItems(),
10      totalPrice: state.getTotalPrice(),
11      itemsCount: state.items.length,
12    }))
13  );
14};
15
16// ✅ Good - single values
17export const useCartIsOpen = () => useCartStore((state) => state.isOpen);
18
19// ❌ Bad - the entire store
20// export const useEntireStore = () => useCartStore();

Subscription pattern

1// hooks/useStoreSubscription.ts
2import { useEffect } from 'react';
3import { useCartStore } from '@/stores/cartStore';
4
5export const useCartAnalytics = () => {
6  useEffect(() => {
7    const unsubscribe = useCartStore.subscribe(
8      (state) => state.items,
9      (items, prevItems) => {
10        // Track analytics when cart changes
11        if (items.length > prevItems.length) {
12          // Item added
13          console.log('Item added to cart');
14        } else if (items.length < prevItems.length) {
15          // Item removed
16          console.log('Item removed from cart');
17        }
18      }
19    );
20
21    return unsubscribe;
22  }, []);
23};

Testing

Store testing

1// __tests__/stores/counterStore.test.ts
2import { renderHook, act } from '@testing-library/react';
3import { useCounterStore } from '@/stores/counterStore';
4
5describe('Counter Store', () => {
6  beforeEach(() => {
7    useCounterStore.setState({ count: 0 });
8  });
9
10  it('should increment count', () => {
11    const { result } = renderHook(() => useCounterStore());
12
13    act(() => {
14      result.current.increment();
15    });
16
17    expect(result.current.count).toBe(1);
18  });
19
20  it('should decrement count', () => {
21    const { result } = renderHook(() => useCounterStore());
22
23    act(() => {
24      result.current.increment();
25      result.current.decrement();
26    });
27
28    expect(result.current.count).toBe(0);
29  });
30
31  it('should reset count', () => {
32    const { result } = renderHook(() => useCounterStore());
33
34    act(() => {
35      result.current.increment();
36      result.current.increment();
37      result.current.reset();
38    });
39
40    expect(result.current.count).toBe(0);
41  });
42});

Best practices

1. Store organization

1// ✅ Good - split stores by topic
2// stores/
3//   ├── userStore.ts      // User authentication & profile
4//   ├── cartStore.ts      // Shopping cart
5//   ├── productsStore.ts  // Products catalog
6//   └── appStore.ts       // Global app state
7
8// ❌ Bad - one giant store
9// stores/globalStore.ts  // Everything in one place

2. TypeScript best practices

1// ✅ Good - define interfaces
2interface UserState {
3  user: User | null;
4  login: (email: string, password: string) => Promise<void>;
5}
6
7// ✅ Good - use generic types
8export const useUserStore = create<UserState>()((set) => ({
9  //
10}));

3. Error handling

1// ✅ Good - handle errors in the store
2const fetchData = async () => {
3  set({ loading: true, error: null });
4  
5  try {
6    const data = await api.fetchData();
7    set({ data, loading: false });
8  } catch (error) {
9    set({ 
10      error: error instanceof Error ? error.message : 'Unknown error',
11      loading: false 
12    });
13  }
14};

Summary

Zustand with Next.js forms a powerful combination for application state management:

  1. Minimalist API - simple and readable
  2. TypeScript-first - full type support
  3. SSR-friendly - works with Server-Side Rendering
  4. Performance - automatic optimizations
  5. Middleware - a rich extension ecosystem

Zustand is the ideal solution for applications that need simple but powerful state management without Redux's boilerplate.

Go to CodeWorlds