cursor-kit-cli 1.1.1 → 1.2.0-beta.2
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/README.md +36 -0
- package/bin/cursor-new-instance +74 -0
- package/bin/cursor-remove-instance +69 -0
- package/dist/cli.cjs +601 -62
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +601 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +39 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/templates/commands/docs.md +5 -3
- package/templates/commands/explain.md +5 -3
- package/templates/commands/fix.md +5 -3
- package/templates/commands/implement.md +5 -3
- package/templates/commands/refactor.md +5 -3
- package/templates/commands/review.md +5 -3
- package/templates/commands/test.md +5 -3
- package/templates/manifest.json +11 -8
- package/templates/rules/git.mdc +0 -2
- package/templates/rules/toc.mdc +17 -9
- package/templates/skills/aesthetic/SKILL.md +121 -0
- package/templates/skills/aesthetic/assets/design-guideline-template.md +163 -0
- package/templates/skills/aesthetic/assets/design-story-template.md +135 -0
- package/templates/skills/aesthetic/references/design-principles.md +62 -0
- package/templates/skills/aesthetic/references/design-resources.md +75 -0
- package/templates/skills/aesthetic/references/micro-interactions.md +53 -0
- package/templates/skills/aesthetic/references/storytelling-design.md +50 -0
- package/templates/skills/backend-development/SKILL.mdc +95 -0
- package/templates/skills/backend-development/references/backend-api-design.md +495 -0
- package/templates/skills/backend-development/references/backend-architecture.md +454 -0
- package/templates/skills/backend-development/references/backend-authentication.md +338 -0
- package/templates/skills/backend-development/references/backend-code-quality.md +659 -0
- package/templates/skills/backend-development/references/backend-debugging.md +904 -0
- package/templates/skills/backend-development/references/backend-devops.md +494 -0
- package/templates/skills/backend-development/references/backend-mindset.md +387 -0
- package/templates/skills/backend-development/references/backend-performance.md +397 -0
- package/templates/skills/backend-development/references/backend-security.md +290 -0
- package/templates/skills/backend-development/references/backend-technologies.md +256 -0
- package/templates/skills/backend-development/references/backend-testing.md +429 -0
- package/templates/skills/frontend-design/SKILL.mdc +41 -0
- package/templates/skills/frontend-design/references/animejs.md +396 -0
- package/templates/skills/frontend-development/SKILL.mdc +399 -0
- package/templates/skills/frontend-development/resources/common-patterns.md +331 -0
- package/templates/skills/frontend-development/resources/complete-examples.md +872 -0
- package/templates/skills/frontend-development/resources/component-patterns.md +502 -0
- package/templates/skills/frontend-development/resources/data-fetching.md +767 -0
- package/templates/skills/frontend-development/resources/file-organization.md +502 -0
- package/templates/skills/frontend-development/resources/loading-and-error-states.md +501 -0
- package/templates/skills/frontend-development/resources/performance.md +406 -0
- package/templates/skills/frontend-development/resources/routing-guide.md +364 -0
- package/templates/skills/frontend-development/resources/styling-guide.md +428 -0
- package/templates/skills/frontend-development/resources/typescript-standards.md +418 -0
- package/templates/skills/problem-solving/SKILL.mdc +96 -0
- package/templates/skills/problem-solving/references/attribution.md +69 -0
- package/templates/skills/problem-solving/references/collision-zone-thinking.md +79 -0
- package/templates/skills/problem-solving/references/inversion-exercise.md +91 -0
- package/templates/skills/problem-solving/references/meta-pattern-recognition.md +87 -0
- package/templates/skills/problem-solving/references/scale-game.md +95 -0
- package/templates/skills/problem-solving/references/simplification-cascades.md +80 -0
- package/templates/skills/problem-solving/references/when-stuck.md +72 -0
- package/templates/skills/research/SKILL.mdc +168 -0
- package/templates/skills/sequential-thinking/.env.example +8 -0
- package/templates/skills/sequential-thinking/README.md +183 -0
- package/templates/skills/sequential-thinking/SKILL.mdc +94 -0
- package/templates/skills/sequential-thinking/package.json +31 -0
- package/templates/skills/sequential-thinking/references/advanced-strategies.md +79 -0
- package/templates/skills/sequential-thinking/references/advanced-techniques.md +76 -0
- package/templates/skills/sequential-thinking/references/core-patterns.md +95 -0
- package/templates/skills/sequential-thinking/references/examples-api.md +88 -0
- package/templates/skills/sequential-thinking/references/examples-architecture.md +94 -0
- package/templates/skills/sequential-thinking/references/examples-debug.md +90 -0
- package/templates/skills/sequential-thinking/scripts/format-thought.js +159 -0
- package/templates/skills/sequential-thinking/scripts/process-thought.js +236 -0
- package/templates/skills/sequential-thinking/tests/format-thought.test.js +133 -0
- package/templates/skills/sequential-thinking/tests/process-thought.test.js +215 -0
- package/templates/skills/ui-styling/LICENSE.txt +202 -0
- package/templates/skills/ui-styling/SKILL.mdc +321 -0
- package/templates/skills/ui-styling/references/canvas-design-system.md +320 -0
- package/templates/skills/ui-styling/references/shadcn-accessibility.md +471 -0
- package/templates/skills/ui-styling/references/shadcn-components.md +424 -0
- package/templates/skills/ui-styling/references/shadcn-theming.md +373 -0
- package/templates/skills/ui-styling/references/tailwind-customization.md +483 -0
- package/templates/skills/ui-styling/references/tailwind-responsive.md +382 -0
- package/templates/skills/ui-styling/references/tailwind-utilities.md +455 -0
- package/templates/rules/frontend-design.mdc +0 -48
- package/templates/rules/performance.mdc +0 -54
- package/templates/rules/react.mdc +0 -58
- package/templates/rules/security.mdc +0 -50
- package/templates/rules/testing.mdc +0 -54
- package/templates/rules/typescript.mdc +0 -36
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
# Data Fetching Patterns
|
|
2
|
+
|
|
3
|
+
Modern data fetching using TanStack Query with Suspense boundaries, cache-first strategies, and centralized API services.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## PRIMARY PATTERN: useSuspenseQuery
|
|
8
|
+
|
|
9
|
+
### Why useSuspenseQuery?
|
|
10
|
+
|
|
11
|
+
For **all new components**, use `useSuspenseQuery` instead of regular `useQuery`:
|
|
12
|
+
|
|
13
|
+
**Benefits:**
|
|
14
|
+
- No `isLoading` checks needed
|
|
15
|
+
- Integrates with Suspense boundaries
|
|
16
|
+
- Cleaner component code
|
|
17
|
+
- Consistent loading UX
|
|
18
|
+
- Better error handling with error boundaries
|
|
19
|
+
|
|
20
|
+
### Basic Pattern
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
24
|
+
import { myFeatureApi } from '../api/myFeatureApi';
|
|
25
|
+
|
|
26
|
+
export const MyComponent: React.FC<Props> = ({ id }) => {
|
|
27
|
+
// No isLoading - Suspense handles it!
|
|
28
|
+
const { data } = useSuspenseQuery({
|
|
29
|
+
queryKey: ['myEntity', id],
|
|
30
|
+
queryFn: () => myFeatureApi.getEntity(id),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// data is ALWAYS defined here (not undefined | Data)
|
|
34
|
+
return <div>{data.name}</div>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Wrap in Suspense boundary
|
|
38
|
+
<SuspenseLoader>
|
|
39
|
+
<MyComponent id={123} />
|
|
40
|
+
</SuspenseLoader>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### useSuspenseQuery vs useQuery
|
|
44
|
+
|
|
45
|
+
| Feature | useSuspenseQuery | useQuery |
|
|
46
|
+
|---------|------------------|----------|
|
|
47
|
+
| Loading state | Handled by Suspense | Manual `isLoading` check |
|
|
48
|
+
| Data type | Always defined | `Data \| undefined` |
|
|
49
|
+
| Use with | Suspense boundaries | Traditional components |
|
|
50
|
+
| Recommended for | **NEW components** | Legacy code only |
|
|
51
|
+
| Error handling | Error boundaries | Manual error state |
|
|
52
|
+
|
|
53
|
+
**When to use regular useQuery:**
|
|
54
|
+
- Maintaining legacy code
|
|
55
|
+
- Very simple cases without Suspense
|
|
56
|
+
- Polling with background updates
|
|
57
|
+
|
|
58
|
+
**For new components: Always prefer useSuspenseQuery**
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Cache-First Strategy
|
|
63
|
+
|
|
64
|
+
### Cache-First Pattern Example
|
|
65
|
+
|
|
66
|
+
**Smart caching** reduces API calls by checking React Query cache first:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query';
|
|
70
|
+
import { postApi } from '../api/postApi';
|
|
71
|
+
|
|
72
|
+
export function useSuspensePost(postId: number) {
|
|
73
|
+
const queryClient = useQueryClient();
|
|
74
|
+
|
|
75
|
+
return useSuspenseQuery({
|
|
76
|
+
queryKey: ['post', postId],
|
|
77
|
+
queryFn: async () => {
|
|
78
|
+
// Strategy 1: Try to get from list cache first
|
|
79
|
+
const cachedListData = queryClient.getQueryData<{ posts: Post[] }>([
|
|
80
|
+
'posts',
|
|
81
|
+
'list'
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
if (cachedListData?.posts) {
|
|
85
|
+
const cachedPost = cachedListData.posts.find(
|
|
86
|
+
(post) => post.id === postId
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (cachedPost) {
|
|
90
|
+
return cachedPost; // Return from cache!
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Strategy 2: Not in cache, fetch from API
|
|
95
|
+
return postApi.getPost(postId);
|
|
96
|
+
},
|
|
97
|
+
staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
|
|
98
|
+
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
|
99
|
+
refetchOnWindowFocus: false, // Don't refetch on focus
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Key Points:**
|
|
105
|
+
- Check grid/list cache before API call
|
|
106
|
+
- Avoids redundant requests
|
|
107
|
+
- `staleTime`: How long data is considered fresh
|
|
108
|
+
- `gcTime`: How long unused data stays in cache
|
|
109
|
+
- `refetchOnWindowFocus: false`: User preference
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Parallel Data Fetching
|
|
114
|
+
|
|
115
|
+
### useSuspenseQueries
|
|
116
|
+
|
|
117
|
+
When fetching multiple independent resources:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { useSuspenseQueries } from '@tanstack/react-query';
|
|
121
|
+
|
|
122
|
+
export const MyComponent: React.FC = () => {
|
|
123
|
+
const [userQuery, settingsQuery, preferencesQuery] = useSuspenseQueries({
|
|
124
|
+
queries: [
|
|
125
|
+
{
|
|
126
|
+
queryKey: ['user'],
|
|
127
|
+
queryFn: () => userApi.getCurrentUser(),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
queryKey: ['settings'],
|
|
131
|
+
queryFn: () => settingsApi.getSettings(),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
queryKey: ['preferences'],
|
|
135
|
+
queryFn: () => preferencesApi.getPreferences(),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// All data available, Suspense handles loading
|
|
141
|
+
const user = userQuery.data;
|
|
142
|
+
const settings = settingsQuery.data;
|
|
143
|
+
const preferences = preferencesQuery.data;
|
|
144
|
+
|
|
145
|
+
return <Display user={user} settings={settings} prefs={preferences} />;
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Benefits:**
|
|
150
|
+
- All queries in parallel
|
|
151
|
+
- Single Suspense boundary
|
|
152
|
+
- Type-safe results
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Query Keys Organization
|
|
157
|
+
|
|
158
|
+
### Naming Convention
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Entity list
|
|
162
|
+
['entities', blogId]
|
|
163
|
+
['entities', blogId, 'summary'] // With view mode
|
|
164
|
+
['entities', blogId, 'flat']
|
|
165
|
+
|
|
166
|
+
// Single entity
|
|
167
|
+
['entity', blogId, entityId]
|
|
168
|
+
|
|
169
|
+
// Related data
|
|
170
|
+
['entity', entityId, 'history']
|
|
171
|
+
['entity', entityId, 'comments']
|
|
172
|
+
|
|
173
|
+
// User-specific
|
|
174
|
+
['user', userId, 'profile']
|
|
175
|
+
['user', userId, 'permissions']
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Rules:**
|
|
179
|
+
- Start with entity name (plural for lists, singular for one)
|
|
180
|
+
- Include IDs for specificity
|
|
181
|
+
- Add view mode / relationship at end
|
|
182
|
+
- Consistent across app
|
|
183
|
+
|
|
184
|
+
### Query Key Examples
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// From useSuspensePost.ts
|
|
188
|
+
queryKey: ['post', blogId, postId]
|
|
189
|
+
queryKey: ['posts-v2', blogId, 'summary']
|
|
190
|
+
|
|
191
|
+
// Invalidation patterns
|
|
192
|
+
queryClient.invalidateQueries({ queryKey: ['post', blogId] }); // All posts for form
|
|
193
|
+
queryClient.invalidateQueries({ queryKey: ['post'] }); // All posts
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## API Service Layer Pattern
|
|
199
|
+
|
|
200
|
+
### File Structure
|
|
201
|
+
|
|
202
|
+
Create centralized API service per feature:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
features/
|
|
206
|
+
my-feature/
|
|
207
|
+
api/
|
|
208
|
+
myFeatureApi.ts # Service layer
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Service Pattern (from postApi.ts)
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
/**
|
|
215
|
+
* Centralized API service for my-feature operations
|
|
216
|
+
* Uses apiClient for consistent error handling
|
|
217
|
+
*/
|
|
218
|
+
import apiClient from '@/lib/apiClient';
|
|
219
|
+
import type { MyEntity, UpdatePayload } from '../types';
|
|
220
|
+
|
|
221
|
+
export const myFeatureApi = {
|
|
222
|
+
/**
|
|
223
|
+
* Fetch a single entity
|
|
224
|
+
*/
|
|
225
|
+
getEntity: async (blogId: number, entityId: number): Promise<MyEntity> => {
|
|
226
|
+
const { data } = await apiClient.get(
|
|
227
|
+
`/blog/entities/${blogId}/${entityId}`
|
|
228
|
+
);
|
|
229
|
+
return data;
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Fetch all entities for a form
|
|
234
|
+
*/
|
|
235
|
+
getEntities: async (blogId: number, view: 'summary' | 'flat'): Promise<MyEntity[]> => {
|
|
236
|
+
const { data } = await apiClient.get(
|
|
237
|
+
`/blog/entities/${blogId}`,
|
|
238
|
+
{ params: { view } }
|
|
239
|
+
);
|
|
240
|
+
return data.rows;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Update entity
|
|
245
|
+
*/
|
|
246
|
+
updateEntity: async (
|
|
247
|
+
blogId: number,
|
|
248
|
+
entityId: number,
|
|
249
|
+
payload: UpdatePayload
|
|
250
|
+
): Promise<MyEntity> => {
|
|
251
|
+
const { data } = await apiClient.put(
|
|
252
|
+
`/blog/entities/${blogId}/${entityId}`,
|
|
253
|
+
payload
|
|
254
|
+
);
|
|
255
|
+
return data;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Delete entity
|
|
260
|
+
*/
|
|
261
|
+
deleteEntity: async (blogId: number, entityId: number): Promise<void> => {
|
|
262
|
+
await apiClient.delete(`/blog/entities/${blogId}/${entityId}`);
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Key Points:**
|
|
268
|
+
- Export single object with methods
|
|
269
|
+
- Use `apiClient` (axios instance from `@/lib/apiClient`)
|
|
270
|
+
- Type-safe parameters and returns
|
|
271
|
+
- JSDoc comments for each method
|
|
272
|
+
- Centralized error handling (apiClient handles it)
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Route Format Rules (IMPORTANT)
|
|
277
|
+
|
|
278
|
+
### Correct Format
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// ✅ CORRECT - Direct service path
|
|
282
|
+
await apiClient.get('/blog/posts/123');
|
|
283
|
+
await apiClient.post('/projects/create', data);
|
|
284
|
+
await apiClient.put('/users/update/456', updates);
|
|
285
|
+
await apiClient.get('/email/templates');
|
|
286
|
+
|
|
287
|
+
// ❌ WRONG - Do NOT add /api/ prefix
|
|
288
|
+
await apiClient.get('/api/blog/posts/123'); // WRONG!
|
|
289
|
+
await apiClient.post('/api/projects/create', data); // WRONG!
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Microservice Routing:**
|
|
293
|
+
- Form service: `/blog/*`
|
|
294
|
+
- Projects service: `/projects/*`
|
|
295
|
+
- Email service: `/email/*`
|
|
296
|
+
- Users service: `/users/*`
|
|
297
|
+
|
|
298
|
+
**Why:** API routing is handled by proxy configuration, no `/api/` prefix needed.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Mutations
|
|
303
|
+
|
|
304
|
+
### Basic Mutation Pattern
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
308
|
+
import { myFeatureApi } from '../api/myFeatureApi';
|
|
309
|
+
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';
|
|
310
|
+
|
|
311
|
+
export const MyComponent: React.FC = () => {
|
|
312
|
+
const queryClient = useQueryClient();
|
|
313
|
+
const { showSuccess, showError } = useMuiSnackbar();
|
|
314
|
+
|
|
315
|
+
const updateMutation = useMutation({
|
|
316
|
+
mutationFn: (payload: UpdatePayload) =>
|
|
317
|
+
myFeatureApi.updateEntity(blogId, entityId, payload),
|
|
318
|
+
|
|
319
|
+
onSuccess: () => {
|
|
320
|
+
// Invalidate and refetch
|
|
321
|
+
queryClient.invalidateQueries({
|
|
322
|
+
queryKey: ['entity', blogId, entityId]
|
|
323
|
+
});
|
|
324
|
+
showSuccess('Entity updated successfully');
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
onError: (error) => {
|
|
328
|
+
showError('Failed to update entity');
|
|
329
|
+
console.error('Update error:', error);
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const handleUpdate = () => {
|
|
334
|
+
updateMutation.mutate({ name: 'New Name' });
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<Button
|
|
339
|
+
onClick={handleUpdate}
|
|
340
|
+
disabled={updateMutation.isPending}
|
|
341
|
+
>
|
|
342
|
+
{updateMutation.isPending ? 'Updating...' : 'Update'}
|
|
343
|
+
</Button>
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Optimistic Updates
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
const updateMutation = useMutation({
|
|
352
|
+
mutationFn: (payload) => myFeatureApi.update(id, payload),
|
|
353
|
+
|
|
354
|
+
// Optimistic update
|
|
355
|
+
onMutate: async (newData) => {
|
|
356
|
+
// Cancel outgoing refetches
|
|
357
|
+
await queryClient.cancelQueries({ queryKey: ['entity', id] });
|
|
358
|
+
|
|
359
|
+
// Snapshot current value
|
|
360
|
+
const previousData = queryClient.getQueryData(['entity', id]);
|
|
361
|
+
|
|
362
|
+
// Optimistically update
|
|
363
|
+
queryClient.setQueryData(['entity', id], (old) => ({
|
|
364
|
+
...old,
|
|
365
|
+
...newData,
|
|
366
|
+
}));
|
|
367
|
+
|
|
368
|
+
// Return rollback function
|
|
369
|
+
return { previousData };
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
// Rollback on error
|
|
373
|
+
onError: (err, newData, context) => {
|
|
374
|
+
queryClient.setQueryData(['entity', id], context.previousData);
|
|
375
|
+
showError('Update failed');
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// Refetch after success or error
|
|
379
|
+
onSettled: () => {
|
|
380
|
+
queryClient.invalidateQueries({ queryKey: ['entity', id] });
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Advanced Query Patterns
|
|
388
|
+
|
|
389
|
+
### Prefetching
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
export function usePrefetchEntity() {
|
|
393
|
+
const queryClient = useQueryClient();
|
|
394
|
+
|
|
395
|
+
return (blogId: number, entityId: number) => {
|
|
396
|
+
return queryClient.prefetchQuery({
|
|
397
|
+
queryKey: ['entity', blogId, entityId],
|
|
398
|
+
queryFn: () => myFeatureApi.getEntity(blogId, entityId),
|
|
399
|
+
staleTime: 5 * 60 * 1000,
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Usage: Prefetch on hover
|
|
405
|
+
<div onMouseEnter={() => prefetch(blogId, id)}>
|
|
406
|
+
<Link to={`/entity/${id}`}>View</Link>
|
|
407
|
+
</div>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Cache Access Without Fetching
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
export function useEntityFromCache(blogId: number, entityId: number) {
|
|
414
|
+
const queryClient = useQueryClient();
|
|
415
|
+
|
|
416
|
+
// Get from cache, don't fetch if missing
|
|
417
|
+
const directCache = queryClient.getQueryData<MyEntity>(['entity', blogId, entityId]);
|
|
418
|
+
|
|
419
|
+
if (directCache) return directCache;
|
|
420
|
+
|
|
421
|
+
// Try grid cache
|
|
422
|
+
const gridCache = queryClient.getQueryData<{ rows: MyEntity[] }>(['entities-v2', blogId]);
|
|
423
|
+
|
|
424
|
+
return gridCache?.rows.find(row => row.id === entityId);
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Dependent Queries
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// Fetch user first, then user's settings
|
|
432
|
+
const { data: user } = useSuspenseQuery({
|
|
433
|
+
queryKey: ['user', userId],
|
|
434
|
+
queryFn: () => userApi.getUser(userId),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const { data: settings } = useSuspenseQuery({
|
|
438
|
+
queryKey: ['user', userId, 'settings'],
|
|
439
|
+
queryFn: () => settingsApi.getUserSettings(user.id),
|
|
440
|
+
// Automatically waits for user to load due to Suspense
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## API Client Configuration
|
|
447
|
+
|
|
448
|
+
### Using apiClient
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import apiClient from '@/lib/apiClient';
|
|
452
|
+
|
|
453
|
+
// apiClient is a configured axios instance
|
|
454
|
+
// Automatically includes:
|
|
455
|
+
// - Base URL configuration
|
|
456
|
+
// - Cookie-based authentication
|
|
457
|
+
// - Error interceptors
|
|
458
|
+
// - Response transformers
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Do NOT create new axios instances** - use apiClient for consistency.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Error Handling in Queries
|
|
466
|
+
|
|
467
|
+
### onError Callback
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';
|
|
471
|
+
|
|
472
|
+
const { showError } = useMuiSnackbar();
|
|
473
|
+
|
|
474
|
+
const { data } = useSuspenseQuery({
|
|
475
|
+
queryKey: ['entity', id],
|
|
476
|
+
queryFn: () => myFeatureApi.getEntity(id),
|
|
477
|
+
|
|
478
|
+
// Handle errors
|
|
479
|
+
onError: (error) => {
|
|
480
|
+
showError('Failed to load entity');
|
|
481
|
+
console.error('Load error:', error);
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Error Boundaries
|
|
487
|
+
|
|
488
|
+
Combine with Error Boundaries for comprehensive error handling:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
492
|
+
|
|
493
|
+
<ErrorBoundary
|
|
494
|
+
fallback={<ErrorDisplay />}
|
|
495
|
+
onError={(error) => console.error(error)}
|
|
496
|
+
>
|
|
497
|
+
<SuspenseLoader>
|
|
498
|
+
<ComponentWithSuspenseQuery />
|
|
499
|
+
</SuspenseLoader>
|
|
500
|
+
</ErrorBoundary>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Complete Examples
|
|
506
|
+
|
|
507
|
+
### Example 1: Simple Entity Fetch
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import React from 'react';
|
|
511
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
512
|
+
import { Box, Typography } from '@mui/material';
|
|
513
|
+
import { userApi } from '../api/userApi';
|
|
514
|
+
|
|
515
|
+
interface UserProfileProps {
|
|
516
|
+
userId: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
|
|
520
|
+
const { data: user } = useSuspenseQuery({
|
|
521
|
+
queryKey: ['user', userId],
|
|
522
|
+
queryFn: () => userApi.getUser(userId),
|
|
523
|
+
staleTime: 5 * 60 * 1000,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<Box>
|
|
528
|
+
<Typography variant='h5'>{user.name}</Typography>
|
|
529
|
+
<Typography>{user.email}</Typography>
|
|
530
|
+
</Box>
|
|
531
|
+
);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Usage with Suspense
|
|
535
|
+
<SuspenseLoader>
|
|
536
|
+
<UserProfile userId='123' />
|
|
537
|
+
</SuspenseLoader>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Example 2: Cache-First Strategy
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query';
|
|
544
|
+
import { postApi } from '../api/postApi';
|
|
545
|
+
import type { Post } from '../types';
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Hook with cache-first strategy
|
|
549
|
+
* Checks grid cache before API call
|
|
550
|
+
*/
|
|
551
|
+
export function useSuspensePost(blogId: number, postId: number) {
|
|
552
|
+
const queryClient = useQueryClient();
|
|
553
|
+
|
|
554
|
+
return useSuspenseQuery<Post, Error>({
|
|
555
|
+
queryKey: ['post', blogId, postId],
|
|
556
|
+
queryFn: async () => {
|
|
557
|
+
// 1. Check grid cache first
|
|
558
|
+
const gridCache = queryClient.getQueryData<{ rows: Post[] }>([
|
|
559
|
+
'posts-v2',
|
|
560
|
+
blogId,
|
|
561
|
+
'summary'
|
|
562
|
+
]) || queryClient.getQueryData<{ rows: Post[] }>([
|
|
563
|
+
'posts-v2',
|
|
564
|
+
blogId,
|
|
565
|
+
'flat'
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
if (gridCache?.rows) {
|
|
569
|
+
const cached = gridCache.rows.find(row => row.S_ID === postId);
|
|
570
|
+
if (cached) {
|
|
571
|
+
return cached; // Reuse grid data
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 2. Not in cache, fetch directly
|
|
576
|
+
return postApi.getPost(blogId, postId);
|
|
577
|
+
},
|
|
578
|
+
staleTime: 5 * 60 * 1000,
|
|
579
|
+
gcTime: 10 * 60 * 1000,
|
|
580
|
+
refetchOnWindowFocus: false,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Benefits:**
|
|
586
|
+
- Avoids duplicate API calls
|
|
587
|
+
- Instant data if already loaded
|
|
588
|
+
- Falls back to API if not cached
|
|
589
|
+
|
|
590
|
+
### Example 3: Parallel Fetching
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import { useSuspenseQueries } from '@tanstack/react-query';
|
|
594
|
+
|
|
595
|
+
export const Dashboard: React.FC = () => {
|
|
596
|
+
const [statsQuery, projectsQuery, notificationsQuery] = useSuspenseQueries({
|
|
597
|
+
queries: [
|
|
598
|
+
{
|
|
599
|
+
queryKey: ['stats'],
|
|
600
|
+
queryFn: () => statsApi.getStats(),
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
queryKey: ['projects', 'active'],
|
|
604
|
+
queryFn: () => projectsApi.getActiveProjects(),
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
queryKey: ['notifications', 'unread'],
|
|
608
|
+
queryFn: () => notificationsApi.getUnread(),
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return (
|
|
614
|
+
<Box>
|
|
615
|
+
<StatsCard data={statsQuery.data} />
|
|
616
|
+
<ProjectsList projects={projectsQuery.data} />
|
|
617
|
+
<Notifications items={notificationsQuery.data} />
|
|
618
|
+
</Box>
|
|
619
|
+
);
|
|
620
|
+
};
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Mutations with Cache Invalidation
|
|
626
|
+
|
|
627
|
+
### Update Mutation
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
631
|
+
import { postApi } from '../api/postApi';
|
|
632
|
+
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';
|
|
633
|
+
|
|
634
|
+
export const useUpdatePost = () => {
|
|
635
|
+
const queryClient = useQueryClient();
|
|
636
|
+
const { showSuccess, showError } = useMuiSnackbar();
|
|
637
|
+
|
|
638
|
+
return useMutation({
|
|
639
|
+
mutationFn: ({ blogId, postId, data }: UpdateParams) =>
|
|
640
|
+
postApi.updatePost(blogId, postId, data),
|
|
641
|
+
|
|
642
|
+
onSuccess: (data, variables) => {
|
|
643
|
+
// Invalidate specific post
|
|
644
|
+
queryClient.invalidateQueries({
|
|
645
|
+
queryKey: ['post', variables.blogId, variables.postId]
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Invalidate list to refresh grid
|
|
649
|
+
queryClient.invalidateQueries({
|
|
650
|
+
queryKey: ['posts-v2', variables.blogId]
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
showSuccess('Post updated');
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
onError: (error) => {
|
|
657
|
+
showError('Failed to update post');
|
|
658
|
+
console.error('Update error:', error);
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// Usage
|
|
664
|
+
const updatePost = useUpdatePost();
|
|
665
|
+
|
|
666
|
+
const handleSave = () => {
|
|
667
|
+
updatePost.mutate({
|
|
668
|
+
blogId: 123,
|
|
669
|
+
postId: 456,
|
|
670
|
+
data: { responses: { '101': 'value' } }
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Delete Mutation
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
export const useDeletePost = () => {
|
|
679
|
+
const queryClient = useQueryClient();
|
|
680
|
+
const { showSuccess, showError } = useMuiSnackbar();
|
|
681
|
+
|
|
682
|
+
return useMutation({
|
|
683
|
+
mutationFn: ({ blogId, postId }: DeleteParams) =>
|
|
684
|
+
postApi.deletePost(blogId, postId),
|
|
685
|
+
|
|
686
|
+
onSuccess: (data, variables) => {
|
|
687
|
+
// Remove from cache manually (optimistic)
|
|
688
|
+
queryClient.setQueryData<{ rows: Post[] }>(
|
|
689
|
+
['posts-v2', variables.blogId],
|
|
690
|
+
(old) => ({
|
|
691
|
+
...old,
|
|
692
|
+
rows: old?.rows.filter(row => row.S_ID !== variables.postId) || []
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
showSuccess('Post deleted');
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
onError: (error, variables) => {
|
|
700
|
+
// Rollback - refetch to get accurate state
|
|
701
|
+
queryClient.invalidateQueries({
|
|
702
|
+
queryKey: ['posts-v2', variables.blogId]
|
|
703
|
+
});
|
|
704
|
+
showError('Failed to delete post');
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## Query Configuration Best Practices
|
|
713
|
+
|
|
714
|
+
### Default Configuration
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
// In QueryClientProvider setup
|
|
718
|
+
const queryClient = new QueryClient({
|
|
719
|
+
defaultOptions: {
|
|
720
|
+
queries: {
|
|
721
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
722
|
+
gcTime: 1000 * 60 * 10, // 10 minutes (was cacheTime)
|
|
723
|
+
refetchOnWindowFocus: false, // Don't refetch on focus
|
|
724
|
+
refetchOnMount: false, // Don't refetch on mount if fresh
|
|
725
|
+
retry: 1, // Retry failed queries once
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Per-Query Overrides
|
|
732
|
+
|
|
733
|
+
```typescript
|
|
734
|
+
// Frequently changing data - shorter staleTime
|
|
735
|
+
useSuspenseQuery({
|
|
736
|
+
queryKey: ['notifications', 'unread'],
|
|
737
|
+
queryFn: () => notificationApi.getUnread(),
|
|
738
|
+
staleTime: 30 * 1000, // 30 seconds
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Rarely changing data - longer staleTime
|
|
742
|
+
useSuspenseQuery({
|
|
743
|
+
queryKey: ['form', blogId, 'structure'],
|
|
744
|
+
queryFn: () => formApi.getStructure(blogId),
|
|
745
|
+
staleTime: 30 * 60 * 1000, // 30 minutes
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## Summary
|
|
752
|
+
|
|
753
|
+
**Modern Data Fetching Recipe:**
|
|
754
|
+
|
|
755
|
+
1. **Create API Service**: `features/X/api/XApi.ts` using apiClient
|
|
756
|
+
2. **Use useSuspenseQuery**: In components wrapped by SuspenseLoader
|
|
757
|
+
3. **Cache-First**: Check grid cache before API call
|
|
758
|
+
4. **Query Keys**: Consistent naming ['entity', id]
|
|
759
|
+
5. **Route Format**: `/blog/route` NOT `/api/blog/route`
|
|
760
|
+
6. **Mutations**: invalidateQueries after success
|
|
761
|
+
7. **Error Handling**: onError + useMuiSnackbar
|
|
762
|
+
8. **Type Safety**: Type all parameters and returns
|
|
763
|
+
|
|
764
|
+
**See Also:**
|
|
765
|
+
- [component-patterns.md](component-patterns.md) - Suspense integration
|
|
766
|
+
- [loading-and-error-states.md](loading-and-error-states.md) - SuspenseLoader usage
|
|
767
|
+
- [complete-examples.md](complete-examples.md) - Full working examples
|