ginskill-init 1.0.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/README.md +77 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +226 -0
- package/package.json +20 -0
- package/skills/ai-asset-generator/SKILL.md +255 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/generate-credit-assets.mjs +180 -0
- package/skills/ai-asset-generator/generate-ginbrowser-assets.mjs +242 -0
- package/skills/ai-asset-generator/generate-sty-icon.mjs +149 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +38 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +88 -0
- package/skills/ai-asset-generator/scripts/scaffold-generator.mjs +203 -0
- package/skills/ai-build-ai/SKILL.md +124 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/scripts/load-tutorial.sh +54 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +321 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/review-code/scripts/check-module.sh +201 -0
- package/skills/review-code/scripts/deep-scan.sh +604 -0
- package/skills/review-code/scripts/dep-check.sh +522 -0
- package/skills/review-code/scripts/detect-duplicates.sh +466 -0
- package/skills/review-code/scripts/format-check.sh +577 -0
- package/skills/review-code/scripts/run-review.sh +167 -0
- package/skills/review-code/scripts/scan-codebase.sh +152 -0
- package/skills/security-scanner/SKILL.md +327 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/security-scanner/scripts/security-scan.sh +478 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-query
|
|
3
|
+
description: |
|
|
4
|
+
**TanStack React Query Best Practices**: Comprehensive guide for writing production-quality React Query code — query keys, mutations, caching, optimistic updates, infinite queries, error handling, and project structure.
|
|
5
|
+
- MANDATORY TRIGGERS: react query, tanstack query, useQuery, useMutation, useInfiniteQuery, query key, query cache, staleTime, gcTime, invalidateQueries, prefetch, optimistic update, server state, data fetching hook, react query pattern, query factory
|
|
6
|
+
- Use this skill whenever the user is writing, reviewing, or debugging React Query / TanStack Query code. Also trigger when discussing data fetching architecture, server state management, cache invalidation, or query performance in React/React Native apps.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TanStack React Query — Best Practices & Patterns
|
|
10
|
+
|
|
11
|
+
Production-ready patterns for TanStack Query (React Query) v5+. Covers query keys, mutations, caching strategy, optimistic updates, infinite queries, error handling, testing, and project structure.
|
|
12
|
+
|
|
13
|
+
## Core Mental Model
|
|
14
|
+
|
|
15
|
+
**Server state ≠ Client state.** Server data is owned by the backend — your frontend merely displays the most recent version. React Query is an async state manager for server state, not a replacement for Zustand/Redux (which handle client state).
|
|
16
|
+
|
|
17
|
+
Key implications:
|
|
18
|
+
- Don't copy query data into local state (`useState`) — you'll lose background updates
|
|
19
|
+
- Don't duplicate query state into Redux/Context — React Query already tracks loading, error, data
|
|
20
|
+
- Treat query data as a **cache** that stays in sync with the server, not a local store you manually manage
|
|
21
|
+
|
|
22
|
+
## Query Key Design
|
|
23
|
+
|
|
24
|
+
Query keys are the foundation of React Query's cache. Get them right and everything else falls into place.
|
|
25
|
+
|
|
26
|
+
### Rules
|
|
27
|
+
|
|
28
|
+
1. **Always use arrays**: `['todos']` not `'todos'`
|
|
29
|
+
2. **Include all dependencies**: If the `queryFn` uses a parameter, that parameter belongs in the key
|
|
30
|
+
3. **Order from general to specific**: `['todos', 'list', { filters }]` not `[{ filters }, 'list', 'todos']`
|
|
31
|
+
4. **Keys are deterministically serialized**: `{ a: 1, b: 2 }` equals `{ b: 2, a: 1 }` in query keys
|
|
32
|
+
|
|
33
|
+
### Query Key Factory Pattern
|
|
34
|
+
|
|
35
|
+
Create one factory per feature/entity. This is the single most impactful pattern for maintainable React Query code.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// keys/todo.keys.ts
|
|
39
|
+
export const todoKeys = {
|
|
40
|
+
all: ['todos'] as const,
|
|
41
|
+
lists: () => [...todoKeys.all, 'list'] as const,
|
|
42
|
+
list: (filters: TodoFilters) => [...todoKeys.lists(), filters] as const,
|
|
43
|
+
details: () => [...todoKeys.all, 'detail'] as const,
|
|
44
|
+
detail: (id: number) => [...todoKeys.details(), id] as const,
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
```typescript
|
|
50
|
+
// Fetch filtered list
|
|
51
|
+
useQuery({ queryKey: todoKeys.list({ status: 'done' }), queryFn: ... })
|
|
52
|
+
|
|
53
|
+
// Fetch single item
|
|
54
|
+
useQuery({ queryKey: todoKeys.detail(5), queryFn: ... })
|
|
55
|
+
|
|
56
|
+
// Invalidate all todos (lists + details)
|
|
57
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.all })
|
|
58
|
+
|
|
59
|
+
// Invalidate only lists (not details)
|
|
60
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
|
|
61
|
+
|
|
62
|
+
// Invalidate one specific list
|
|
63
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.list({ status: 'done' }) })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Co-location
|
|
67
|
+
|
|
68
|
+
Keep query keys, hooks, and `queryFn` together per feature — not in a global `queryKeys.ts`:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
src/
|
|
72
|
+
├── features/
|
|
73
|
+
│ ├── todos/
|
|
74
|
+
│ │ ├── todo.keys.ts # query key factory
|
|
75
|
+
│ │ ├── todo.queries.ts # useQuery hooks
|
|
76
|
+
│ │ ├── todo.mutations.ts # useMutation hooks
|
|
77
|
+
│ │ └── todo.types.ts # types
|
|
78
|
+
│ └── users/
|
|
79
|
+
│ ├── user.keys.ts
|
|
80
|
+
│ ├── user.queries.ts
|
|
81
|
+
│ └── user.mutations.ts
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or if using a `_services` pattern (common in React Native projects):
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
src/
|
|
88
|
+
├── models/
|
|
89
|
+
│ ├── todo/
|
|
90
|
+
│ │ ├── _services/
|
|
91
|
+
│ │ │ ├── get-todos.service.ts
|
|
92
|
+
│ │ │ ├── create-todo.service.ts
|
|
93
|
+
│ │ │ └── delete-todo.service.ts
|
|
94
|
+
│ │ ├── _types/
|
|
95
|
+
│ │ │ └── todo.types.ts
|
|
96
|
+
│ │ └── todo.keys.ts
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Custom Hooks — Always Wrap
|
|
100
|
+
|
|
101
|
+
**Never use `useQuery` / `useMutation` directly in components.** Always wrap in a custom hook.
|
|
102
|
+
|
|
103
|
+
### Why
|
|
104
|
+
|
|
105
|
+
- Single place to change the query key, queryFn, or options
|
|
106
|
+
- Consumers don't need to know API details
|
|
107
|
+
- Easy to add `select`, `enabled`, `staleTime` per use-case
|
|
108
|
+
- Testable in isolation
|
|
109
|
+
|
|
110
|
+
### Query Hook Pattern
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// todo.queries.ts
|
|
114
|
+
import { useQuery } from '@tanstack/react-query'
|
|
115
|
+
import { todoKeys } from './todo.keys'
|
|
116
|
+
import { fetchTodos, fetchTodoById } from './todo.api'
|
|
117
|
+
import type { TodoFilters } from './todo.types'
|
|
118
|
+
|
|
119
|
+
export const useTodos = (filters: TodoFilters) => {
|
|
120
|
+
return useQuery({
|
|
121
|
+
queryKey: todoKeys.list(filters),
|
|
122
|
+
queryFn: () => fetchTodos(filters),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const useTodo = (id: number) => {
|
|
127
|
+
return useQuery({
|
|
128
|
+
queryKey: todoKeys.detail(id),
|
|
129
|
+
queryFn: () => fetchTodoById(id),
|
|
130
|
+
enabled: !!id, // don't fetch if id is falsy
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Mutation Hook Pattern
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// todo.mutations.ts
|
|
139
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
140
|
+
import { todoKeys } from './todo.keys'
|
|
141
|
+
import { createTodo, updateTodo, deleteTodo } from './todo.api'
|
|
142
|
+
|
|
143
|
+
export const useCreateTodo = () => {
|
|
144
|
+
const queryClient = useQueryClient()
|
|
145
|
+
|
|
146
|
+
return useMutation({
|
|
147
|
+
mutationFn: createTodo,
|
|
148
|
+
onSuccess: () => {
|
|
149
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const useUpdateTodo = () => {
|
|
155
|
+
const queryClient = useQueryClient()
|
|
156
|
+
|
|
157
|
+
return useMutation({
|
|
158
|
+
mutationFn: updateTodo,
|
|
159
|
+
onSuccess: (data, variables) => {
|
|
160
|
+
// Update the detail cache directly
|
|
161
|
+
queryClient.setQueryData(todoKeys.detail(variables.id), data)
|
|
162
|
+
// Invalidate lists so they refetch
|
|
163
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const useDeleteTodo = () => {
|
|
169
|
+
const queryClient = useQueryClient()
|
|
170
|
+
|
|
171
|
+
return useMutation({
|
|
172
|
+
mutationFn: deleteTodo,
|
|
173
|
+
onSuccess: () => {
|
|
174
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Adding Optimistic Updates to Mutation Services
|
|
181
|
+
|
|
182
|
+
For mutations where instant UI feedback matters (toggle, delete, reorder), add optimistic updates **inside the mutation hook** — not in the component. This keeps the optimistic logic co-located with the mutation and reusable across all consumers.
|
|
183
|
+
|
|
184
|
+
**Basic mutation** (wait for server) vs **Optimistic mutation** (update immediately, rollback on error):
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// todo.mutations.ts — optimistic version
|
|
188
|
+
|
|
189
|
+
export const useToggleTodo = () => {
|
|
190
|
+
const queryClient = useQueryClient()
|
|
191
|
+
|
|
192
|
+
return useMutation({
|
|
193
|
+
mutationFn: (todo: Todo) => updateTodo({ ...todo, completed: !todo.completed }),
|
|
194
|
+
|
|
195
|
+
// Step 1: Optimistically update cache BEFORE the API call
|
|
196
|
+
onMutate: async (todo) => {
|
|
197
|
+
// Cancel in-flight refetches so they don't overwrite our optimistic update
|
|
198
|
+
await queryClient.cancelQueries({ queryKey: todoKeys.detail(todo.id) })
|
|
199
|
+
await queryClient.cancelQueries({ queryKey: todoKeys.lists() })
|
|
200
|
+
|
|
201
|
+
// Snapshot current cache for rollback
|
|
202
|
+
const previousTodo = queryClient.getQueryData<Todo>(todoKeys.detail(todo.id))
|
|
203
|
+
const previousList = queryClient.getQueryData<Todo[]>(todoKeys.lists())
|
|
204
|
+
|
|
205
|
+
// Write optimistic data to cache — UI updates instantly
|
|
206
|
+
const optimistic = { ...todo, completed: !todo.completed }
|
|
207
|
+
queryClient.setQueryData(todoKeys.detail(todo.id), optimistic)
|
|
208
|
+
queryClient.setQueryData<Todo[]>(todoKeys.lists(), (old) =>
|
|
209
|
+
old?.map(t => t.id === todo.id ? optimistic : t)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Return rollback context
|
|
213
|
+
return { previousTodo, previousList }
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Step 2: Rollback on error
|
|
217
|
+
onError: (_err, todo, context) => {
|
|
218
|
+
if (context?.previousTodo) {
|
|
219
|
+
queryClient.setQueryData(todoKeys.detail(todo.id), context.previousTodo)
|
|
220
|
+
}
|
|
221
|
+
if (context?.previousList) {
|
|
222
|
+
queryClient.setQueryData(todoKeys.lists(), context.previousList)
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// Step 3: Always refetch after mutation settles to ensure server truth
|
|
227
|
+
onSettled: () => {
|
|
228
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.all })
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Optimistic delete — remove from list immediately
|
|
234
|
+
export const useDeleteTodoOptimistic = () => {
|
|
235
|
+
const queryClient = useQueryClient()
|
|
236
|
+
|
|
237
|
+
return useMutation({
|
|
238
|
+
mutationFn: deleteTodo,
|
|
239
|
+
onMutate: async (todoId: number) => {
|
|
240
|
+
await queryClient.cancelQueries({ queryKey: todoKeys.lists() })
|
|
241
|
+
const previousList = queryClient.getQueryData<Todo[]>(todoKeys.lists())
|
|
242
|
+
|
|
243
|
+
// Remove from cache immediately
|
|
244
|
+
queryClient.setQueryData<Todo[]>(todoKeys.lists(), (old) =>
|
|
245
|
+
old?.filter(t => t.id !== todoId)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return { previousList }
|
|
249
|
+
},
|
|
250
|
+
onError: (_err, _todoId, context) => {
|
|
251
|
+
if (context?.previousList) {
|
|
252
|
+
queryClient.setQueryData(todoKeys.lists(), context.previousList)
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
onSettled: () => {
|
|
256
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**When to use optimistic updates in mutation services:**
|
|
263
|
+
|
|
264
|
+
| Scenario | Use optimistic? | Why |
|
|
265
|
+
|----------|----------------|-----|
|
|
266
|
+
| Toggle (like, bookmark, complete) | Yes | Instant feedback feels natural, easy to rollback |
|
|
267
|
+
| Delete item from list | Yes | Item disappearing immediately feels responsive |
|
|
268
|
+
| Reorder / drag-and-drop | Yes | Must feel instant, server confirms in background |
|
|
269
|
+
| Create new item | Usually no | Wait for server ID, show pending state via `mutation.isPending` |
|
|
270
|
+
| Payment / checkout | Never | Must confirm server success before showing result |
|
|
271
|
+
| File upload | No | Use `mutation.isPending` + progress indicator instead |
|
|
272
|
+
|
|
273
|
+
**Component usage is clean — no optimistic logic leaks out:**
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Component — doesn't know about optimistic updates
|
|
277
|
+
const { mutate: toggleTodo } = useToggleTodo()
|
|
278
|
+
const { mutate: deleteTodo } = useDeleteTodoOptimistic()
|
|
279
|
+
|
|
280
|
+
<TodoItem
|
|
281
|
+
onToggle={() => toggleTodo(todo)}
|
|
282
|
+
onDelete={() => deleteTodo(todo.id)}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Caching Strategy
|
|
287
|
+
|
|
288
|
+
### staleTime vs gcTime
|
|
289
|
+
|
|
290
|
+
| Setting | Default | What it controls |
|
|
291
|
+
|---------|---------|-----------------|
|
|
292
|
+
| `staleTime` | `0` | How long data is considered "fresh". While fresh, no refetch happens — data served from cache only |
|
|
293
|
+
| `gcTime` | `5 min` | How long inactive (no observers) data stays in cache before garbage collection |
|
|
294
|
+
|
|
295
|
+
### Recommended Defaults
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const queryClient = new QueryClient({
|
|
299
|
+
defaultOptions: {
|
|
300
|
+
queries: {
|
|
301
|
+
staleTime: 1000 * 60, // 1 minute — prevents redundant refetches
|
|
302
|
+
gcTime: 1000 * 60 * 5, // 5 minutes (default)
|
|
303
|
+
retry: 2, // retry failed requests twice
|
|
304
|
+
refetchOnWindowFocus: true, // keep data fresh (default)
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Per-Query Overrides
|
|
311
|
+
|
|
312
|
+
| Data type | staleTime | Why |
|
|
313
|
+
|-----------|-----------|-----|
|
|
314
|
+
| User profile | `5 min` | Rarely changes within a session |
|
|
315
|
+
| Config / feature flags | `Infinity` | Fetch once, use forever |
|
|
316
|
+
| Chat messages | `0` | Must always be fresh |
|
|
317
|
+
| Product list | `30s - 1min` | Balance freshness vs. API load |
|
|
318
|
+
| Dashboard analytics | `5 min` | Expensive query, acceptable staleness |
|
|
319
|
+
|
|
320
|
+
## Data Transformation with `select`
|
|
321
|
+
|
|
322
|
+
Transform or filter data **in the query hook**, not in the component:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Good — select runs only when data changes (referentially stable)
|
|
326
|
+
export const useCompletedTodos = () => {
|
|
327
|
+
return useQuery({
|
|
328
|
+
queryKey: todoKeys.list({ status: 'all' }),
|
|
329
|
+
queryFn: () => fetchTodos({ status: 'all' }),
|
|
330
|
+
select: (data) => data.filter(todo => todo.completed),
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Bad — filtering in component runs on every render
|
|
335
|
+
const { data } = useTodos({ status: 'all' })
|
|
336
|
+
const completed = data?.filter(todo => todo.completed) // re-computed every render
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`select` benefits:
|
|
340
|
+
- Only runs when `data` reference changes
|
|
341
|
+
- Result is memoized
|
|
342
|
+
- Component only re-renders when the selected result changes
|
|
343
|
+
- Multiple components can use the same query with different `select` functions
|
|
344
|
+
|
|
345
|
+
## Optimistic Updates
|
|
346
|
+
|
|
347
|
+
### Method 1: Via UI (simpler, preferred for single-location updates)
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
const mutation = useCreateTodo()
|
|
351
|
+
|
|
352
|
+
// In JSX, show optimistic data directly from mutation state
|
|
353
|
+
{mutation.isPending && (
|
|
354
|
+
<TodoItem todo={{ ...mutation.variables, id: 'temp' }} style={{ opacity: 0.5 }} />
|
|
355
|
+
)}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Method 2: Via Cache (for updates visible in multiple places)
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
export const useUpdateTodo = () => {
|
|
362
|
+
const queryClient = useQueryClient()
|
|
363
|
+
|
|
364
|
+
return useMutation({
|
|
365
|
+
mutationFn: updateTodo,
|
|
366
|
+
onMutate: async (newTodo) => {
|
|
367
|
+
// Cancel outgoing refetches to avoid overwriting optimistic update
|
|
368
|
+
await queryClient.cancelQueries({ queryKey: todoKeys.detail(newTodo.id) })
|
|
369
|
+
|
|
370
|
+
// Snapshot previous value for rollback
|
|
371
|
+
const previous = queryClient.getQueryData(todoKeys.detail(newTodo.id))
|
|
372
|
+
|
|
373
|
+
// Optimistically update the cache
|
|
374
|
+
queryClient.setQueryData(todoKeys.detail(newTodo.id), newTodo)
|
|
375
|
+
|
|
376
|
+
// Return context with previous value
|
|
377
|
+
return { previous }
|
|
378
|
+
},
|
|
379
|
+
onError: (_err, newTodo, context) => {
|
|
380
|
+
// Rollback on error
|
|
381
|
+
if (context?.previous) {
|
|
382
|
+
queryClient.setQueryData(todoKeys.detail(newTodo.id), context.previous)
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
onSettled: (_data, _error, variables) => {
|
|
386
|
+
// Refetch to ensure server truth
|
|
387
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.detail(variables.id) })
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Infinite Queries
|
|
394
|
+
|
|
395
|
+
For "load more" or infinite scroll lists:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
export const useTodoInfinite = (filters: TodoFilters) => {
|
|
399
|
+
return useInfiniteQuery({
|
|
400
|
+
queryKey: todoKeys.list({ ...filters, infinite: true }),
|
|
401
|
+
queryFn: ({ pageParam }) => fetchTodos({ ...filters, cursor: pageParam }),
|
|
402
|
+
initialPageParam: undefined as string | undefined,
|
|
403
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
404
|
+
// Limit stored pages to prevent memory bloat
|
|
405
|
+
maxPages: 10,
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// In component
|
|
410
|
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useTodoInfinite(filters)
|
|
411
|
+
|
|
412
|
+
const allItems = data?.pages.flatMap(page => page.items) ?? []
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Tips
|
|
416
|
+
- Use `maxPages` to cap memory usage for long lists
|
|
417
|
+
- `placeholderData: keepPreviousData` prevents flash of empty state during page transitions
|
|
418
|
+
- Prefetch the next page: `queryClient.prefetchInfiniteQuery(...)` one page ahead
|
|
419
|
+
|
|
420
|
+
## Dependent & Conditional Queries
|
|
421
|
+
|
|
422
|
+
### Sequential (dependent) queries
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const { data: user } = useUser(userId)
|
|
426
|
+
const { data: projects } = useQuery({
|
|
427
|
+
queryKey: ['projects', user?.orgId],
|
|
428
|
+
queryFn: () => fetchProjects(user!.orgId),
|
|
429
|
+
enabled: !!user?.orgId, // only run when user data is available
|
|
430
|
+
})
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Conditional (user-triggered) queries
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
437
|
+
|
|
438
|
+
const { data } = useQuery({
|
|
439
|
+
queryKey: ['search', searchTerm],
|
|
440
|
+
queryFn: () => search(searchTerm),
|
|
441
|
+
enabled: searchTerm.length >= 3, // only search after 3 chars
|
|
442
|
+
})
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Prefetching
|
|
446
|
+
|
|
447
|
+
### On hover (for navigation)
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const queryClient = useQueryClient()
|
|
451
|
+
|
|
452
|
+
const prefetchTodo = (id: number) => {
|
|
453
|
+
queryClient.prefetchQuery({
|
|
454
|
+
queryKey: todoKeys.detail(id),
|
|
455
|
+
queryFn: () => fetchTodoById(id),
|
|
456
|
+
staleTime: 1000 * 60, // don't refetch if data < 1 min old
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
<Link onMouseEnter={() => prefetchTodo(todo.id)} to={`/todos/${todo.id}`}>
|
|
461
|
+
{todo.title}
|
|
462
|
+
</Link>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### On screen mount (prefetch related data)
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// In a list screen, prefetch the first few detail pages
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
data?.slice(0, 3).forEach(todo => {
|
|
471
|
+
queryClient.prefetchQuery({
|
|
472
|
+
queryKey: todoKeys.detail(todo.id),
|
|
473
|
+
queryFn: () => fetchTodoById(todo.id),
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
}, [data])
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Error Handling
|
|
480
|
+
|
|
481
|
+
### Per-query error handling
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
const { data, error, isError } = useTodos(filters)
|
|
485
|
+
|
|
486
|
+
if (isError) return <ErrorMessage error={error} />
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Global error handler
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const queryClient = new QueryClient({
|
|
493
|
+
defaultOptions: {
|
|
494
|
+
queries: {
|
|
495
|
+
throwOnError: true, // propagate to nearest Error Boundary
|
|
496
|
+
},
|
|
497
|
+
mutations: {
|
|
498
|
+
onError: (error) => {
|
|
499
|
+
toast.error(error.message) // global toast for mutation errors
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Error Boundary integration
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
import { QueryErrorResetBoundary } from '@tanstack/react-query'
|
|
510
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
511
|
+
|
|
512
|
+
<QueryErrorResetBoundary>
|
|
513
|
+
{({ reset }) => (
|
|
514
|
+
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
|
|
515
|
+
<View>
|
|
516
|
+
<Text>Something went wrong</Text>
|
|
517
|
+
<Button onPress={resetErrorBoundary} title="Try again" />
|
|
518
|
+
</View>
|
|
519
|
+
)}>
|
|
520
|
+
<TodoList />
|
|
521
|
+
</ErrorBoundary>
|
|
522
|
+
)}
|
|
523
|
+
</QueryErrorResetBoundary>
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Common Anti-Patterns
|
|
527
|
+
|
|
528
|
+
### 1. Duplicating query data into Redux/Zustand
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// BAD — duplicates state, loses background sync
|
|
532
|
+
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
|
|
533
|
+
useEffect(() => { dispatch(setTodos(data)) }, [data])
|
|
534
|
+
|
|
535
|
+
// GOOD — use query data directly
|
|
536
|
+
const { data: todos, isLoading } = useTodos()
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### 2. Using `refetch()` when the key should change
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
// BAD — refetch is imperative, creates race conditions
|
|
543
|
+
const [page, setPage] = useState(1)
|
|
544
|
+
const { data, refetch } = useQuery({
|
|
545
|
+
queryKey: ['todos'], // key doesn't include page!
|
|
546
|
+
queryFn: () => fetchTodos(page),
|
|
547
|
+
})
|
|
548
|
+
const next = () => { setPage(p => p + 1); refetch() }
|
|
549
|
+
|
|
550
|
+
// GOOD — key includes page, auto-refetches on change
|
|
551
|
+
const { data } = useQuery({
|
|
552
|
+
queryKey: ['todos', page], // page in key
|
|
553
|
+
queryFn: () => fetchTodos(page),
|
|
554
|
+
})
|
|
555
|
+
const next = () => setPage(p => p + 1) // that's it
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 3. Transforming data in components with useEffect
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// BAD — extra state, extra renders, stale data risk
|
|
562
|
+
const { data } = useTodos()
|
|
563
|
+
const [filtered, setFiltered] = useState([])
|
|
564
|
+
useEffect(() => { setFiltered(data?.filter(t => t.done)) }, [data])
|
|
565
|
+
|
|
566
|
+
// GOOD — use select
|
|
567
|
+
const { data: filtered } = useTodos({ select: d => d.filter(t => t.done) })
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### 4. Copying query data into form state incorrectly
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
// BAD — form never gets server updates
|
|
574
|
+
const { data } = useTodo(id)
|
|
575
|
+
const [form, setForm] = useState(data) // snapshot at mount time
|
|
576
|
+
|
|
577
|
+
// GOOD — use initialData or defaultValues, set staleTime: Infinity for forms
|
|
578
|
+
const { data } = useTodo(id)
|
|
579
|
+
const form = useForm({ defaultValues: data })
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### 5. Creating QueryClient inside a component
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
// BAD — new cache every render
|
|
586
|
+
function App() {
|
|
587
|
+
const queryClient = new QueryClient() // recreated on every render!
|
|
588
|
+
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// GOOD — stable instance
|
|
592
|
+
const queryClient = new QueryClient()
|
|
593
|
+
function App() {
|
|
594
|
+
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// GOOD (React 19 / strict mode safe)
|
|
598
|
+
function App() {
|
|
599
|
+
const [queryClient] = useState(() => new QueryClient())
|
|
600
|
+
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### 6. Not disabling queries when parameters are missing
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
// BAD — fires with undefined id, returns 404
|
|
608
|
+
const { data } = useQuery({
|
|
609
|
+
queryKey: ['todo', id],
|
|
610
|
+
queryFn: () => fetchTodo(id!),
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
// GOOD — wait for id
|
|
614
|
+
const { data } = useQuery({
|
|
615
|
+
queryKey: ['todo', id],
|
|
616
|
+
queryFn: () => fetchTodo(id!),
|
|
617
|
+
enabled: !!id,
|
|
618
|
+
})
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## React Native–Specific Tips
|
|
622
|
+
|
|
623
|
+
### Online status
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
import NetInfo from '@react-native-community/netinfo'
|
|
627
|
+
import { onlineManager } from '@tanstack/react-query'
|
|
628
|
+
|
|
629
|
+
onlineManager.setEventListener((setOnline) => {
|
|
630
|
+
return NetInfo.addEventListener((state) => {
|
|
631
|
+
setOnline(!!state.isConnected)
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### App focus refetch
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
import { useEffect } from 'react'
|
|
640
|
+
import { AppState } from 'react-native'
|
|
641
|
+
import { focusManager } from '@tanstack/react-query'
|
|
642
|
+
|
|
643
|
+
useEffect(() => {
|
|
644
|
+
const subscription = AppState.addEventListener('change', (status) => {
|
|
645
|
+
focusManager.setFocused(status === 'active')
|
|
646
|
+
})
|
|
647
|
+
return () => subscription.remove()
|
|
648
|
+
}, [])
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Persist cache (offline-first)
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
|
|
655
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
656
|
+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
|
657
|
+
|
|
658
|
+
const persister = createAsyncStoragePersister({
|
|
659
|
+
storage: AsyncStorage,
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
<PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>
|
|
663
|
+
<App />
|
|
664
|
+
</PersistQueryClientProvider>
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## Quick Reference
|
|
668
|
+
|
|
669
|
+
| Task | API |
|
|
670
|
+
|------|-----|
|
|
671
|
+
| Fetch data | `useQuery` |
|
|
672
|
+
| Fetch paginated/infinite | `useInfiniteQuery` |
|
|
673
|
+
| Create / Update / Delete | `useMutation` |
|
|
674
|
+
| Invalidate cache | `queryClient.invalidateQueries()` |
|
|
675
|
+
| Update cache directly | `queryClient.setQueryData()` |
|
|
676
|
+
| Prefetch | `queryClient.prefetchQuery()` |
|
|
677
|
+
| Cancel queries | `queryClient.cancelQueries()` |
|
|
678
|
+
| Check if fetching | `useIsFetching()` |
|
|
679
|
+
| Suspense mode | `useSuspenseQuery()` |
|
|
680
|
+
| Error boundaries | `<QueryErrorResetBoundary>` |
|
|
681
|
+
|
|
682
|
+
## Further Reading
|
|
683
|
+
|
|
684
|
+
For detailed reference on specific topics, see:
|
|
685
|
+
- `references/query-patterns.md` — Advanced patterns: parallel queries, dependent queries, pagination, polling, SSR hydration
|