red64-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +89 -3
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# State Management Patterns
|
|
2
|
+
|
|
3
|
+
Modern state management for React applications using Zustand for client state and TanStack Query for server state.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Separate server and client state**: Server state (API data) and client state (UI state) have different needs
|
|
10
|
+
- **Minimal global state**: Keep state local when possible; lift only when necessary
|
|
11
|
+
- **Single source of truth**: Each piece of state lives in exactly one place
|
|
12
|
+
- **Derived state over stored state**: Compute values from existing state rather than storing duplicates
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## State Categories
|
|
17
|
+
|
|
18
|
+
| Category | Definition | Tool | Examples |
|
|
19
|
+
|----------|------------|------|----------|
|
|
20
|
+
| **Server State** | Data from external sources | TanStack Query | Users list, product data, API responses |
|
|
21
|
+
| **Client State** | UI and application state | Zustand | Sidebar open, theme, shopping cart |
|
|
22
|
+
| **Local State** | Component-specific state | useState | Form inputs, toggles, modals |
|
|
23
|
+
| **URL State** | Route and search params | React Router | Current page, filters, search query |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Zustand for Client State
|
|
28
|
+
|
|
29
|
+
### Basic Store
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// stores/ui.store.ts
|
|
33
|
+
import { create } from 'zustand';
|
|
34
|
+
|
|
35
|
+
interface UIState {
|
|
36
|
+
sidebarOpen: boolean;
|
|
37
|
+
theme: 'light' | 'dark' | 'system';
|
|
38
|
+
toggleSidebar: () => void;
|
|
39
|
+
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const useUIStore = create<UIState>((set) => ({
|
|
43
|
+
sidebarOpen: true,
|
|
44
|
+
theme: 'system',
|
|
45
|
+
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
|
46
|
+
setTheme: (theme) => set({ theme }),
|
|
47
|
+
}));
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Store with Persistence
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// stores/auth.store.ts
|
|
54
|
+
import { create } from 'zustand';
|
|
55
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
56
|
+
|
|
57
|
+
interface AuthState {
|
|
58
|
+
token: string | null;
|
|
59
|
+
user: User | null;
|
|
60
|
+
setAuth: (token: string, user: User) => void;
|
|
61
|
+
clearAuth: () => void;
|
|
62
|
+
isAuthenticated: () => boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const useAuthStore = create<AuthState>()(
|
|
66
|
+
persist(
|
|
67
|
+
(set, get) => ({
|
|
68
|
+
token: null,
|
|
69
|
+
user: null,
|
|
70
|
+
setAuth: (token, user) => set({ token, user }),
|
|
71
|
+
clearAuth: () => set({ token: null, user: null }),
|
|
72
|
+
isAuthenticated: () => get().token !== null,
|
|
73
|
+
}),
|
|
74
|
+
{
|
|
75
|
+
name: 'auth-storage',
|
|
76
|
+
storage: createJSONStorage(() => localStorage),
|
|
77
|
+
partialize: (state) => ({ token: state.token, user: state.user }), // Only persist these fields
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Store with Immer (Complex Updates)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// stores/cart.store.ts
|
|
87
|
+
import { create } from 'zustand';
|
|
88
|
+
import { immer } from 'zustand/middleware/immer';
|
|
89
|
+
|
|
90
|
+
interface CartItem {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
price: number;
|
|
94
|
+
quantity: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface CartState {
|
|
98
|
+
items: CartItem[];
|
|
99
|
+
addItem: (item: Omit<CartItem, 'quantity'>) => void;
|
|
100
|
+
removeItem: (id: string) => void;
|
|
101
|
+
updateQuantity: (id: string, quantity: number) => void;
|
|
102
|
+
clearCart: () => void;
|
|
103
|
+
total: () => number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const useCartStore = create<CartState>()(
|
|
107
|
+
immer((set, get) => ({
|
|
108
|
+
items: [],
|
|
109
|
+
|
|
110
|
+
addItem: (item) =>
|
|
111
|
+
set((state) => {
|
|
112
|
+
const existing = state.items.find((i) => i.id === item.id);
|
|
113
|
+
if (existing) {
|
|
114
|
+
existing.quantity += 1;
|
|
115
|
+
} else {
|
|
116
|
+
state.items.push({ ...item, quantity: 1 });
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
|
|
120
|
+
removeItem: (id) =>
|
|
121
|
+
set((state) => {
|
|
122
|
+
state.items = state.items.filter((i) => i.id !== id);
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
updateQuantity: (id, quantity) =>
|
|
126
|
+
set((state) => {
|
|
127
|
+
const item = state.items.find((i) => i.id === id);
|
|
128
|
+
if (item) {
|
|
129
|
+
item.quantity = Math.max(0, quantity);
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
clearCart: () => set({ items: [] }),
|
|
134
|
+
|
|
135
|
+
total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
136
|
+
}))
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Slices Pattern (Large Stores)
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// stores/slices/userSlice.ts
|
|
144
|
+
import type { StateCreator } from 'zustand';
|
|
145
|
+
|
|
146
|
+
export interface UserSlice {
|
|
147
|
+
user: User | null;
|
|
148
|
+
setUser: (user: User | null) => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const createUserSlice: StateCreator<UserSlice> = (set) => ({
|
|
152
|
+
user: null,
|
|
153
|
+
setUser: (user) => set({ user }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// stores/slices/preferencesSlice.ts
|
|
157
|
+
export interface PreferencesSlice {
|
|
158
|
+
theme: 'light' | 'dark';
|
|
159
|
+
language: string;
|
|
160
|
+
setTheme: (theme: 'light' | 'dark') => void;
|
|
161
|
+
setLanguage: (lang: string) => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const createPreferencesSlice: StateCreator<PreferencesSlice> = (set) => ({
|
|
165
|
+
theme: 'light',
|
|
166
|
+
language: 'en',
|
|
167
|
+
setTheme: (theme) => set({ theme }),
|
|
168
|
+
setLanguage: (language) => set({ language }),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// stores/app.store.ts
|
|
172
|
+
import { create } from 'zustand';
|
|
173
|
+
import { createUserSlice, type UserSlice } from './slices/userSlice';
|
|
174
|
+
import { createPreferencesSlice, type PreferencesSlice } from './slices/preferencesSlice';
|
|
175
|
+
|
|
176
|
+
type AppStore = UserSlice & PreferencesSlice;
|
|
177
|
+
|
|
178
|
+
export const useAppStore = create<AppStore>()((...a) => ({
|
|
179
|
+
...createUserSlice(...a),
|
|
180
|
+
...createPreferencesSlice(...a),
|
|
181
|
+
}));
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Zustand Best Practices
|
|
187
|
+
|
|
188
|
+
### Selectors for Performance
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// BAD - Subscribes to entire store, re-renders on any change
|
|
192
|
+
const { sidebarOpen, theme, user } = useUIStore();
|
|
193
|
+
|
|
194
|
+
// GOOD - Only subscribes to what's needed
|
|
195
|
+
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
|
|
196
|
+
const theme = useUIStore((state) => state.theme);
|
|
197
|
+
|
|
198
|
+
// GOOD - Multiple values with shallow comparison
|
|
199
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
200
|
+
|
|
201
|
+
const { sidebarOpen, theme } = useUIStore(
|
|
202
|
+
useShallow((state) => ({ sidebarOpen: state.sidebarOpen, theme: state.theme }))
|
|
203
|
+
);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Actions Outside Components
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// Access store outside React
|
|
210
|
+
const token = useAuthStore.getState().token;
|
|
211
|
+
useAuthStore.getState().setAuth(newToken, user);
|
|
212
|
+
|
|
213
|
+
// Useful in API interceptors, service functions
|
|
214
|
+
export const api = ky.create({
|
|
215
|
+
hooks: {
|
|
216
|
+
beforeRequest: [
|
|
217
|
+
(request) => {
|
|
218
|
+
const token = useAuthStore.getState().token;
|
|
219
|
+
if (token) {
|
|
220
|
+
request.headers.set('Authorization', `Bearer ${token}`);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Derived State
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// BAD - Storing computed values
|
|
232
|
+
interface CartState {
|
|
233
|
+
items: CartItem[];
|
|
234
|
+
total: number; // Don't store this!
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// GOOD - Compute from source
|
|
238
|
+
interface CartState {
|
|
239
|
+
items: CartItem[];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Option 1: Method in store
|
|
243
|
+
export const useCartStore = create<CartState>((set, get) => ({
|
|
244
|
+
items: [],
|
|
245
|
+
// Computed value as method
|
|
246
|
+
getTotal: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
247
|
+
}));
|
|
248
|
+
|
|
249
|
+
// Option 2: Selector
|
|
250
|
+
const total = useCartStore((state) =>
|
|
251
|
+
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Local State (useState)
|
|
258
|
+
|
|
259
|
+
Use local state when:
|
|
260
|
+
- State is only used by one component
|
|
261
|
+
- State doesn't need to persist
|
|
262
|
+
- State is UI-specific (hover, focus, animation)
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Modal open state - local to component
|
|
266
|
+
function UserActions({ user }: { user: User }) {
|
|
267
|
+
const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<>
|
|
271
|
+
<Button onClick={() => setDeleteModalOpen(true)}>Delete</Button>
|
|
272
|
+
<DeleteModal
|
|
273
|
+
open={isDeleteModalOpen}
|
|
274
|
+
onClose={() => setDeleteModalOpen(false)}
|
|
275
|
+
user={user}
|
|
276
|
+
/>
|
|
277
|
+
</>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## useReducer for Complex Local State
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// Complex state with multiple related values
|
|
288
|
+
type FormState = {
|
|
289
|
+
values: Record<string, string>;
|
|
290
|
+
errors: Record<string, string>;
|
|
291
|
+
touched: Record<string, boolean>;
|
|
292
|
+
isSubmitting: boolean;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
type FormAction =
|
|
296
|
+
| { type: 'SET_VALUE'; field: string; value: string }
|
|
297
|
+
| { type: 'SET_ERROR'; field: string; error: string }
|
|
298
|
+
| { type: 'SET_TOUCHED'; field: string }
|
|
299
|
+
| { type: 'START_SUBMIT' }
|
|
300
|
+
| { type: 'END_SUBMIT' }
|
|
301
|
+
| { type: 'RESET' };
|
|
302
|
+
|
|
303
|
+
function formReducer(state: FormState, action: FormAction): FormState {
|
|
304
|
+
switch (action.type) {
|
|
305
|
+
case 'SET_VALUE':
|
|
306
|
+
return {
|
|
307
|
+
...state,
|
|
308
|
+
values: { ...state.values, [action.field]: action.value },
|
|
309
|
+
errors: { ...state.errors, [action.field]: '' },
|
|
310
|
+
};
|
|
311
|
+
case 'SET_ERROR':
|
|
312
|
+
return {
|
|
313
|
+
...state,
|
|
314
|
+
errors: { ...state.errors, [action.field]: action.error },
|
|
315
|
+
};
|
|
316
|
+
case 'SET_TOUCHED':
|
|
317
|
+
return {
|
|
318
|
+
...state,
|
|
319
|
+
touched: { ...state.touched, [action.field]: true },
|
|
320
|
+
};
|
|
321
|
+
case 'START_SUBMIT':
|
|
322
|
+
return { ...state, isSubmitting: true };
|
|
323
|
+
case 'END_SUBMIT':
|
|
324
|
+
return { ...state, isSubmitting: false };
|
|
325
|
+
case 'RESET':
|
|
326
|
+
return initialState;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Context for Dependency Injection
|
|
334
|
+
|
|
335
|
+
Use Context for:
|
|
336
|
+
- Dependency injection (providing services, clients)
|
|
337
|
+
- Theme/i18n that rarely changes
|
|
338
|
+
- Feature flags
|
|
339
|
+
|
|
340
|
+
**Don't use for frequently changing state** (causes unnecessary re-renders).
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// contexts/ApiContext.tsx
|
|
344
|
+
import { createContext, useContext, type ReactNode } from 'react';
|
|
345
|
+
import type { ApiClient } from '@/services/api';
|
|
346
|
+
|
|
347
|
+
const ApiContext = createContext<ApiClient | null>(null);
|
|
348
|
+
|
|
349
|
+
export function ApiProvider({ client, children }: { client: ApiClient; children: ReactNode }) {
|
|
350
|
+
return <ApiContext.Provider value={client}>{children}</ApiContext.Provider>;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function useApiClient(): ApiClient {
|
|
354
|
+
const client = useContext(ApiContext);
|
|
355
|
+
if (!client) {
|
|
356
|
+
throw new Error('useApiClient must be used within ApiProvider');
|
|
357
|
+
}
|
|
358
|
+
return client;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## URL State (React Router)
|
|
365
|
+
|
|
366
|
+
Store state in URL when:
|
|
367
|
+
- State should be shareable via link
|
|
368
|
+
- State should survive page refresh
|
|
369
|
+
- State represents the current "view" (filters, pagination, search)
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Using search params for filters
|
|
373
|
+
import { useSearchParams } from 'react-router-dom';
|
|
374
|
+
|
|
375
|
+
function UserList() {
|
|
376
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
377
|
+
|
|
378
|
+
const page = Number(searchParams.get('page')) || 1;
|
|
379
|
+
const sort = searchParams.get('sort') || 'name';
|
|
380
|
+
const search = searchParams.get('q') || '';
|
|
381
|
+
|
|
382
|
+
const setPage = (newPage: number) => {
|
|
383
|
+
setSearchParams((params) => {
|
|
384
|
+
params.set('page', String(newPage));
|
|
385
|
+
return params;
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const setSearch = (query: string) => {
|
|
390
|
+
setSearchParams((params) => {
|
|
391
|
+
if (query) {
|
|
392
|
+
params.set('q', query);
|
|
393
|
+
} else {
|
|
394
|
+
params.delete('q');
|
|
395
|
+
}
|
|
396
|
+
params.set('page', '1'); // Reset to first page
|
|
397
|
+
return params;
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Use page, sort, search in your query
|
|
402
|
+
const { data } = useUsers({ page, sort, search });
|
|
403
|
+
|
|
404
|
+
return (/* ... */);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## State Decision Flowchart
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
Is this data from an API?
|
|
414
|
+
├── Yes → TanStack Query (server state)
|
|
415
|
+
└── No → Is it needed by multiple components?
|
|
416
|
+
├── No → useState or useReducer (local state)
|
|
417
|
+
└── Yes → Should it be in the URL?
|
|
418
|
+
├── Yes → useSearchParams (URL state)
|
|
419
|
+
└── No → Does it persist across sessions?
|
|
420
|
+
├── Yes → Zustand with persist
|
|
421
|
+
└── No → Zustand (client state)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Anti-Patterns
|
|
427
|
+
|
|
428
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
429
|
+
|--------------|---------|------------------|
|
|
430
|
+
| Storing API data in Zustand | Stale data, no caching | Use TanStack Query for server state |
|
|
431
|
+
| Global state for everything | Unnecessary re-renders | Keep state local when possible |
|
|
432
|
+
| Context for frequent updates | Performance issues | Use Zustand with selectors |
|
|
433
|
+
| Duplicate state | Sync issues | Single source of truth, derive when needed |
|
|
434
|
+
| Not using selectors | Unnecessary re-renders | Always select specific values |
|
|
435
|
+
| Storing derived values | Can become stale | Compute on read |
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Testing Stores
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// Reset store between tests
|
|
443
|
+
import { useCartStore } from './cart.store';
|
|
444
|
+
|
|
445
|
+
beforeEach(() => {
|
|
446
|
+
useCartStore.setState({ items: [] });
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('adds item to cart', () => {
|
|
450
|
+
const { addItem } = useCartStore.getState();
|
|
451
|
+
|
|
452
|
+
addItem({ id: '1', name: 'Product', price: 100 });
|
|
453
|
+
|
|
454
|
+
expect(useCartStore.getState().items).toHaveLength(1);
|
|
455
|
+
expect(useCartStore.getState().items[0].quantity).toBe(1);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('increments quantity for existing item', () => {
|
|
459
|
+
useCartStore.setState({
|
|
460
|
+
items: [{ id: '1', name: 'Product', price: 100, quantity: 1 }],
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const { addItem } = useCartStore.getState();
|
|
464
|
+
addItem({ id: '1', name: 'Product', price: 100 });
|
|
465
|
+
|
|
466
|
+
expect(useCartStore.getState().items[0].quantity).toBe(2);
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
_State is data plus the rules for changing it. Keep it minimal, local, and predictable._
|