W Metropolii Quantum, gdzie stan aplikacji jest zarządzany z precyzją kwantową, poznasz Zustand - minimalistyczną, ale potężną bibliotekę do zarządzania stanem. Ta kombinacja z Next.js tworzy eleganckie rozwiązanie do budowania skalowalnych aplikacji z przejrzystym flow danych.
Zustand to nowoczesna biblioteka state management, która oferuje:
1# Zainstaluj Zustand
2npm install zustand
3
4# Dla development tools (opcjonalne)
5npm install @redux-devtools/extension1// 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}));1// components/Counter.tsx
2'use client';
3
4import { useCounterStore } from '@/stores/counterStore';
5
6export default function Counter() {
7 const { count, increment, 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">Licznik: {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}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('Nieprawidłowe dane logowania');
43 }
44
45 const userData = await response.json();
46 set({ user: userData.user, isLoading: false });
47 } catch (error) {
48 set({
49 error: error instanceof Error ? error.message : 'Wystąpił błąd',
50 isLoading: false
51 });
52 }
53 },
54
55 logout: () => {
56 set({ user: null, error: null });
57 // Wywołaj API logout
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('Nie udało się zaktualizować profilu');
76 }
77
78 const updatedUser = await response.json();
79 set({ user: updatedUser, isLoading: false });
80 } catch (error) {
81 set({
82 error: error instanceof Error ? error.message : 'Wystąpił błąd',
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 tylko user data
95 }
96 )
97);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, item) => total + item.quantity, 0);
82 },
83
84 getTotalPrice: () => {
85 return get().items.reduce(
86 (total, item) => total + item.price * item.quantity,
87 0
88 );
89 },
90 }),
91 {
92 name: 'cart-storage',
93 }
94 )
95);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, id, 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);1npm install immer1// 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);1// providers/StoreProvider.tsx
2'use client';
3
4import { type ReactNode, createContext, useRef, useContext } from 'react';
5import { useStore } from 'zustand';
6import { type UserStore, createUserStore, initUserStore } 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, initialState }: 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};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};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}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} zł</p>
62
63 {item.variant && (
64 <p className="text-sm text-gray-500">
65 {item.variant.size && `Rozmiar: ${item.variant.size}`}
66 {item.variant.color && ` Kolor: ${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, item.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, item.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">Łącznie:</span>
105 <span className="font-semibold">{getTotalPrice()} zł</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 Przejdź do kasy
110 </button>
111 </div>
112 )}
113 </div>
114 </div>
115 );
116}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}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 || '');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}));1// hooks/useOptimizedSelectors.ts
2import { useShallow } from 'zustand/react/shallow';
3import { useCartStore } from '@/stores/cartStore';
4
5// ✅ Dobrze - używaj selektorów
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// ✅ Dobrze - pojedyncze wartości
17export const useCartIsOpen = () => useCartStore((state) => state.isOpen);
18
19// ❌ Źle - cały store
20// export const useEntireStore = () => useCartStore();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};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});1// ✅ Dobrze - podziel store'y tematycznie
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// ❌ Źle - jeden gigantyczny store
9// stores/globalStore.ts // Everything in one place1// ✅ Dobrze - definiuj interfaces
2interface UserState {
3 user: User | null;
4 login: (email: string, password: string) => Promise<void>;
5}
6
7// ✅ Dobrze - używaj typów generycznych
8export const useUserStore = create<UserState>()((set) => ({
9 //
10}));1// ✅ Dobrze - obsługuj błędy w 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};Zustand z Next.js tworzy potężną kombinację dla zarządzania stanem aplikacji:
Zustand to idealne rozwiązanie dla aplikacji, które potrzebują prostego, ale potężnego state management bez boilerplate kodu Redux.