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,586 @@
|
|
|
1
|
+
# Data Fetching Patterns
|
|
2
|
+
|
|
3
|
+
Server state management with TanStack Query for caching, synchronization, and background updates.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Server state is different**: API data needs caching, deduplication, background refresh
|
|
10
|
+
- **Cache-first**: Serve stale data immediately, refresh in background
|
|
11
|
+
- **Optimistic updates**: Update UI before server confirms for better UX
|
|
12
|
+
- **Error boundaries**: Handle errors at the right level, not in every component
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## TanStack Query Setup
|
|
17
|
+
|
|
18
|
+
### Query Client Configuration
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// lib/query-client.ts
|
|
22
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
23
|
+
|
|
24
|
+
export const queryClient = new QueryClient({
|
|
25
|
+
defaultOptions: {
|
|
26
|
+
queries: {
|
|
27
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
28
|
+
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
|
|
29
|
+
retry: 3,
|
|
30
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
31
|
+
refetchOnWindowFocus: true,
|
|
32
|
+
refetchOnReconnect: true,
|
|
33
|
+
},
|
|
34
|
+
mutations: {
|
|
35
|
+
retry: 1,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Provider Setup
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// app/App.tsx
|
|
45
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
46
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
47
|
+
import { queryClient } from '@/lib/query-client';
|
|
48
|
+
|
|
49
|
+
export function App() {
|
|
50
|
+
return (
|
|
51
|
+
<QueryClientProvider client={queryClient}>
|
|
52
|
+
<RouterProvider router={router} />
|
|
53
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
54
|
+
</QueryClientProvider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Query Patterns
|
|
62
|
+
|
|
63
|
+
### Basic Query
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// features/users/hooks/useUsers.ts
|
|
67
|
+
import { useQuery } from '@tanstack/react-query';
|
|
68
|
+
import { api } from '@/services/api';
|
|
69
|
+
import type { User, PaginatedResponse } from '@/types';
|
|
70
|
+
|
|
71
|
+
// Query key factory
|
|
72
|
+
export const userKeys = {
|
|
73
|
+
all: ['users'] as const,
|
|
74
|
+
lists: () => [...userKeys.all, 'list'] as const,
|
|
75
|
+
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
|
|
76
|
+
details: () => [...userKeys.all, 'detail'] as const,
|
|
77
|
+
detail: (id: number) => [...userKeys.details(), id] as const,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface UserFilters {
|
|
81
|
+
page?: number;
|
|
82
|
+
search?: string;
|
|
83
|
+
role?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fetchUsers(filters: UserFilters): Promise<PaginatedResponse<User>> {
|
|
87
|
+
const params = new URLSearchParams();
|
|
88
|
+
if (filters.page) params.set('page', String(filters.page));
|
|
89
|
+
if (filters.search) params.set('q', filters.search);
|
|
90
|
+
if (filters.role) params.set('role', filters.role);
|
|
91
|
+
|
|
92
|
+
return api.get(`users?${params}`).json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function useUsers(filters: UserFilters = {}) {
|
|
96
|
+
return useQuery({
|
|
97
|
+
queryKey: userKeys.list(filters),
|
|
98
|
+
queryFn: () => fetchUsers(filters),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Single Resource Query
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// features/users/hooks/useUser.ts
|
|
107
|
+
async function fetchUser(id: number): Promise<User> {
|
|
108
|
+
return api.get(`users/${id}`).json();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useUser(id: number) {
|
|
112
|
+
return useQuery({
|
|
113
|
+
queryKey: userKeys.detail(id),
|
|
114
|
+
queryFn: () => fetchUser(id),
|
|
115
|
+
enabled: id > 0, // Don't fetch if no ID
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Dependent Queries
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// Fetch user, then fetch their posts
|
|
124
|
+
function useUserWithPosts(userId: number) {
|
|
125
|
+
const userQuery = useUser(userId);
|
|
126
|
+
|
|
127
|
+
const postsQuery = useQuery({
|
|
128
|
+
queryKey: ['users', userId, 'posts'],
|
|
129
|
+
queryFn: () => fetchUserPosts(userId),
|
|
130
|
+
enabled: !!userQuery.data, // Only fetch when user is loaded
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return { user: userQuery, posts: postsQuery };
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Parallel Queries
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { useQueries } from '@tanstack/react-query';
|
|
141
|
+
|
|
142
|
+
function useUserDetails(userIds: number[]) {
|
|
143
|
+
return useQueries({
|
|
144
|
+
queries: userIds.map((id) => ({
|
|
145
|
+
queryKey: userKeys.detail(id),
|
|
146
|
+
queryFn: () => fetchUser(id),
|
|
147
|
+
})),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Mutation Patterns
|
|
155
|
+
|
|
156
|
+
### Basic Mutation
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// features/users/hooks/useCreateUser.ts
|
|
160
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
161
|
+
|
|
162
|
+
interface CreateUserInput {
|
|
163
|
+
name: string;
|
|
164
|
+
email: string;
|
|
165
|
+
role: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function createUser(input: CreateUserInput): Promise<User> {
|
|
169
|
+
return api.post('users', { json: input }).json();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function useCreateUser() {
|
|
173
|
+
const queryClient = useQueryClient();
|
|
174
|
+
|
|
175
|
+
return useMutation({
|
|
176
|
+
mutationFn: createUser,
|
|
177
|
+
onSuccess: () => {
|
|
178
|
+
// Invalidate and refetch user list
|
|
179
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Update Mutation with Optimistic Updates
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// features/users/hooks/useUpdateUser.ts
|
|
189
|
+
interface UpdateUserInput {
|
|
190
|
+
id: number;
|
|
191
|
+
data: Partial<User>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function updateUser({ id, data }: UpdateUserInput): Promise<User> {
|
|
195
|
+
return api.patch(`users/${id}`, { json: data }).json();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function useUpdateUser() {
|
|
199
|
+
const queryClient = useQueryClient();
|
|
200
|
+
|
|
201
|
+
return useMutation({
|
|
202
|
+
mutationFn: updateUser,
|
|
203
|
+
// Optimistic update
|
|
204
|
+
onMutate: async ({ id, data }) => {
|
|
205
|
+
// Cancel outgoing refetches
|
|
206
|
+
await queryClient.cancelQueries({ queryKey: userKeys.detail(id) });
|
|
207
|
+
|
|
208
|
+
// Snapshot previous value
|
|
209
|
+
const previousUser = queryClient.getQueryData<User>(userKeys.detail(id));
|
|
210
|
+
|
|
211
|
+
// Optimistically update
|
|
212
|
+
if (previousUser) {
|
|
213
|
+
queryClient.setQueryData<User>(userKeys.detail(id), {
|
|
214
|
+
...previousUser,
|
|
215
|
+
...data,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { previousUser };
|
|
220
|
+
},
|
|
221
|
+
onError: (_err, { id }, context) => {
|
|
222
|
+
// Rollback on error
|
|
223
|
+
if (context?.previousUser) {
|
|
224
|
+
queryClient.setQueryData(userKeys.detail(id), context.previousUser);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
onSettled: (_data, _error, { id }) => {
|
|
228
|
+
// Refetch to ensure server state
|
|
229
|
+
queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
|
|
230
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Delete Mutation
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
export function useDeleteUser() {
|
|
240
|
+
const queryClient = useQueryClient();
|
|
241
|
+
|
|
242
|
+
return useMutation({
|
|
243
|
+
mutationFn: (id: number) => api.delete(`users/${id}`),
|
|
244
|
+
onSuccess: () => {
|
|
245
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Usage in Components
|
|
254
|
+
|
|
255
|
+
### Query Component
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// features/users/components/UserList.tsx
|
|
259
|
+
import { useUsers } from '../hooks/useUsers';
|
|
260
|
+
|
|
261
|
+
export function UserList({ filters }: { filters: UserFilters }) {
|
|
262
|
+
const { data, isLoading, isError, error, refetch } = useUsers(filters);
|
|
263
|
+
|
|
264
|
+
if (isLoading) {
|
|
265
|
+
return <LoadingSpinner />;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (isError) {
|
|
269
|
+
return (
|
|
270
|
+
<ErrorMessage
|
|
271
|
+
message={error.message}
|
|
272
|
+
onRetry={() => refetch()}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!data?.items.length) {
|
|
278
|
+
return <EmptyState message="No users found" />;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<ul>
|
|
283
|
+
{data.items.map((user) => (
|
|
284
|
+
<UserCard key={user.id} user={user} />
|
|
285
|
+
))}
|
|
286
|
+
</ul>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Mutation Component
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// features/users/components/CreateUserForm.tsx
|
|
295
|
+
import { useCreateUser } from '../hooks/useCreateUser';
|
|
296
|
+
|
|
297
|
+
export function CreateUserForm({ onSuccess }: { onSuccess?: () => void }) {
|
|
298
|
+
const createUser = useCreateUser();
|
|
299
|
+
|
|
300
|
+
const handleSubmit = (data: CreateUserInput) => {
|
|
301
|
+
createUser.mutate(data, {
|
|
302
|
+
onSuccess: () => {
|
|
303
|
+
toast.success('User created');
|
|
304
|
+
onSuccess?.();
|
|
305
|
+
},
|
|
306
|
+
onError: (error) => {
|
|
307
|
+
toast.error(error.message);
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
|
314
|
+
{/* Form fields */}
|
|
315
|
+
<Button type="submit" disabled={createUser.isPending}>
|
|
316
|
+
{createUser.isPending ? 'Creating...' : 'Create User'}
|
|
317
|
+
</Button>
|
|
318
|
+
</form>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## API Client Setup
|
|
326
|
+
|
|
327
|
+
### ky Configuration
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// services/api.ts
|
|
331
|
+
import ky from 'ky';
|
|
332
|
+
import { useAuthStore } from '@/stores/auth.store';
|
|
333
|
+
|
|
334
|
+
export const api = ky.create({
|
|
335
|
+
prefixUrl: import.meta.env.VITE_API_URL,
|
|
336
|
+
timeout: 30000,
|
|
337
|
+
hooks: {
|
|
338
|
+
beforeRequest: [
|
|
339
|
+
(request) => {
|
|
340
|
+
const token = useAuthStore.getState().token;
|
|
341
|
+
if (token) {
|
|
342
|
+
request.headers.set('Authorization', `Bearer ${token}`);
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
afterResponse: [
|
|
347
|
+
async (_request, _options, response) => {
|
|
348
|
+
if (response.status === 401) {
|
|
349
|
+
useAuthStore.getState().clearAuth();
|
|
350
|
+
window.location.href = '/login';
|
|
351
|
+
}
|
|
352
|
+
return response;
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Type-safe API with Zod
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// features/users/api/users.api.ts
|
|
363
|
+
import { z } from 'zod';
|
|
364
|
+
import { api } from '@/services/api';
|
|
365
|
+
|
|
366
|
+
const UserSchema = z.object({
|
|
367
|
+
id: z.number(),
|
|
368
|
+
name: z.string(),
|
|
369
|
+
email: z.string().email(),
|
|
370
|
+
role: z.enum(['admin', 'user', 'guest']),
|
|
371
|
+
createdAt: z.string().datetime(),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const PaginatedUsersSchema = z.object({
|
|
375
|
+
items: z.array(UserSchema),
|
|
376
|
+
total: z.number(),
|
|
377
|
+
page: z.number(),
|
|
378
|
+
perPage: z.number(),
|
|
379
|
+
hasNext: z.boolean(),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
export type User = z.infer<typeof UserSchema>;
|
|
383
|
+
export type PaginatedUsers = z.infer<typeof PaginatedUsersSchema>;
|
|
384
|
+
|
|
385
|
+
export async function fetchUsers(filters: UserFilters): Promise<PaginatedUsers> {
|
|
386
|
+
const response = await api.get('users', { searchParams: filters }).json();
|
|
387
|
+
return PaginatedUsersSchema.parse(response); // Runtime validation
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Prefetching and Preloading
|
|
394
|
+
|
|
395
|
+
### Prefetch on Hover
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
function UserListItem({ user }: { user: User }) {
|
|
399
|
+
const queryClient = useQueryClient();
|
|
400
|
+
|
|
401
|
+
const prefetchUser = () => {
|
|
402
|
+
queryClient.prefetchQuery({
|
|
403
|
+
queryKey: userKeys.detail(user.id),
|
|
404
|
+
queryFn: () => fetchUser(user.id),
|
|
405
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<Link
|
|
411
|
+
to={`/users/${user.id}`}
|
|
412
|
+
onMouseEnter={prefetchUser}
|
|
413
|
+
onFocus={prefetchUser}
|
|
414
|
+
>
|
|
415
|
+
{user.name}
|
|
416
|
+
</Link>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Prefetch on Route Load
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// app/routes.tsx
|
|
425
|
+
import { queryClient } from '@/lib/query-client';
|
|
426
|
+
|
|
427
|
+
export const router = createBrowserRouter([
|
|
428
|
+
{
|
|
429
|
+
path: '/users/:id',
|
|
430
|
+
element: <UserDetail />,
|
|
431
|
+
loader: async ({ params }) => {
|
|
432
|
+
const id = Number(params.id);
|
|
433
|
+
// Prefetch data before rendering
|
|
434
|
+
await queryClient.ensureQueryData({
|
|
435
|
+
queryKey: userKeys.detail(id),
|
|
436
|
+
queryFn: () => fetchUser(id),
|
|
437
|
+
});
|
|
438
|
+
return null;
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Infinite Queries (Pagination)
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// features/posts/hooks/usePosts.ts
|
|
450
|
+
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
451
|
+
|
|
452
|
+
export function usePosts() {
|
|
453
|
+
return useInfiniteQuery({
|
|
454
|
+
queryKey: ['posts'],
|
|
455
|
+
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
|
|
456
|
+
initialPageParam: undefined as string | undefined,
|
|
457
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
458
|
+
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Usage
|
|
463
|
+
function PostList() {
|
|
464
|
+
const {
|
|
465
|
+
data,
|
|
466
|
+
fetchNextPage,
|
|
467
|
+
hasNextPage,
|
|
468
|
+
isFetchingNextPage,
|
|
469
|
+
} = usePosts();
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<>
|
|
473
|
+
{data?.pages.map((page) =>
|
|
474
|
+
page.items.map((post) => <PostCard key={post.id} post={post} />)
|
|
475
|
+
)}
|
|
476
|
+
{hasNextPage && (
|
|
477
|
+
<Button
|
|
478
|
+
onClick={() => fetchNextPage()}
|
|
479
|
+
disabled={isFetchingNextPage}
|
|
480
|
+
>
|
|
481
|
+
{isFetchingNextPage ? 'Loading...' : 'Load More'}
|
|
482
|
+
</Button>
|
|
483
|
+
)}
|
|
484
|
+
</>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Error Handling
|
|
492
|
+
|
|
493
|
+
### Query Error Boundary
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// components/QueryErrorBoundary.tsx
|
|
497
|
+
import { QueryErrorResetBoundary } from '@tanstack/react-query';
|
|
498
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
499
|
+
|
|
500
|
+
export function QueryErrorBoundary({ children }: { children: React.ReactNode }) {
|
|
501
|
+
return (
|
|
502
|
+
<QueryErrorResetBoundary>
|
|
503
|
+
{({ reset }) => (
|
|
504
|
+
<ErrorBoundary
|
|
505
|
+
onReset={reset}
|
|
506
|
+
fallbackRender={({ error, resetErrorBoundary }) => (
|
|
507
|
+
<div className="error-container">
|
|
508
|
+
<p>Something went wrong: {error.message}</p>
|
|
509
|
+
<Button onClick={resetErrorBoundary}>Try again</Button>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
>
|
|
513
|
+
{children}
|
|
514
|
+
</ErrorBoundary>
|
|
515
|
+
)}
|
|
516
|
+
</QueryErrorResetBoundary>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Global Error Handling
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// lib/query-client.ts
|
|
525
|
+
export const queryClient = new QueryClient({
|
|
526
|
+
defaultOptions: {
|
|
527
|
+
queries: {
|
|
528
|
+
throwOnError: (error) => {
|
|
529
|
+
// Only throw for specific errors to be caught by boundary
|
|
530
|
+
return error instanceof NetworkError;
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
mutations: {
|
|
534
|
+
onError: (error) => {
|
|
535
|
+
// Global mutation error handling
|
|
536
|
+
toast.error(error.message);
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Suspense Mode
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
// Enable suspense for a query
|
|
549
|
+
export function useUser(id: number) {
|
|
550
|
+
return useSuspenseQuery({
|
|
551
|
+
queryKey: userKeys.detail(id),
|
|
552
|
+
queryFn: () => fetchUser(id),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Usage with Suspense boundary
|
|
557
|
+
function UserPage({ userId }: { userId: number }) {
|
|
558
|
+
return (
|
|
559
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
560
|
+
<UserDetail userId={userId} />
|
|
561
|
+
</Suspense>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function UserDetail({ userId }: { userId: number }) {
|
|
566
|
+
const { data: user } = useUser(userId); // Never undefined with suspense
|
|
567
|
+
return <div>{user.name}</div>;
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Anti-Patterns
|
|
574
|
+
|
|
575
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
576
|
+
|--------------|---------|------------------|
|
|
577
|
+
| Storing query data in Zustand | Double source of truth | Let TanStack Query manage server state |
|
|
578
|
+
| No query keys | Cache conflicts | Use query key factories |
|
|
579
|
+
| Fetching in useEffect | Missing caching, deduplication | Use useQuery |
|
|
580
|
+
| Manual refetch everywhere | Stale data | Configure staleTime, use invalidation |
|
|
581
|
+
| Not handling loading/error | Bad UX | Always handle all states |
|
|
582
|
+
| Inline query functions | No reuse, hard to test | Extract to separate files |
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
_Server state is not your state. TanStack Query manages it better than you can._
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Feedback Configuration
|
|
2
|
+
|
|
3
|
+
Project-specific commands for automated feedback during React implementation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Test Commands
|
|
8
|
+
|
|
9
|
+
Commands to run tests during implementation. The agent will use these to verify code changes.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# Primary test command (REQUIRED)
|
|
13
|
+
test: pnpm test:run
|
|
14
|
+
|
|
15
|
+
# Test with coverage report
|
|
16
|
+
test_coverage: pnpm test:coverage
|
|
17
|
+
|
|
18
|
+
# Run specific test file (use {file} as placeholder)
|
|
19
|
+
test_file: pnpm test:run {file}
|
|
20
|
+
|
|
21
|
+
# Watch mode (for development)
|
|
22
|
+
test_watch: pnpm test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Linting Commands
|
|
28
|
+
|
|
29
|
+
Commands for code quality checks.
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
# Primary lint command
|
|
33
|
+
lint: pnpm lint
|
|
34
|
+
|
|
35
|
+
# Lint with auto-fix
|
|
36
|
+
lint_fix: pnpm lint --fix
|
|
37
|
+
|
|
38
|
+
# Type checking
|
|
39
|
+
type_check: pnpm tsc --noEmit
|
|
40
|
+
|
|
41
|
+
# Format check
|
|
42
|
+
format_check: pnpm prettier --check .
|
|
43
|
+
|
|
44
|
+
# Format fix
|
|
45
|
+
format_fix: pnpm prettier --write .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Development Server
|
|
51
|
+
|
|
52
|
+
Commands for starting the development server (required for UI verification).
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
# Start dev server (Vite)
|
|
56
|
+
dev_server: pnpm dev
|
|
57
|
+
|
|
58
|
+
# Dev server port
|
|
59
|
+
dev_port: 5173
|
|
60
|
+
|
|
61
|
+
# Dev server base URL
|
|
62
|
+
dev_url: http://localhost:5173
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## UI Verification
|
|
68
|
+
|
|
69
|
+
Settings for agent-browser UI verification.
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
# Enable UI verification for this project
|
|
73
|
+
ui_verification_enabled: true
|
|
74
|
+
|
|
75
|
+
# Default wait time after navigation (milliseconds)
|
|
76
|
+
navigation_wait: 3000
|
|
77
|
+
|
|
78
|
+
# Screenshot directory
|
|
79
|
+
screenshot_dir: /tmp/ui-captures
|
|
80
|
+
|
|
81
|
+
# Storybook URL (if using Storybook)
|
|
82
|
+
storybook_url: http://localhost:6006
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## E2E Testing
|
|
88
|
+
|
|
89
|
+
End-to-end testing with Playwright.
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
# Run E2E tests
|
|
93
|
+
e2e: pnpm test:e2e
|
|
94
|
+
|
|
95
|
+
# Run E2E with UI
|
|
96
|
+
e2e_ui: pnpm test:e2e --ui
|
|
97
|
+
|
|
98
|
+
# Run E2E for specific browser
|
|
99
|
+
e2e_chromium: pnpm test:e2e --project=chromium
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Notes
|
|
105
|
+
|
|
106
|
+
- Uses Vitest for unit/integration tests (faster than Jest)
|
|
107
|
+
- Uses Playwright for E2E testing
|
|
108
|
+
- pnpm is the default package manager (faster, disk-efficient)
|
|
109
|
+
- Vite dev server on port 5173 by default
|
|
110
|
+
- UI verification is enabled by default for React projects
|