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.
Files changed (92) hide show
  1. package/README.md +77 -0
  2. package/agents/developer.md +56 -0
  3. package/agents/frontend-design.md +69 -0
  4. package/agents/mobile-reviewer.md +36 -0
  5. package/agents/review-code.md +49 -0
  6. package/agents/security-scanner.md +50 -0
  7. package/agents/tester.md +72 -0
  8. package/bin/cli.js +226 -0
  9. package/package.json +20 -0
  10. package/skills/ai-asset-generator/SKILL.md +255 -0
  11. package/skills/ai-asset-generator/docs/gen-image.md +274 -0
  12. package/skills/ai-asset-generator/docs/genvideo.md +341 -0
  13. package/skills/ai-asset-generator/docs/remove-background.md +19 -0
  14. package/skills/ai-asset-generator/generate-credit-assets.mjs +180 -0
  15. package/skills/ai-asset-generator/generate-ginbrowser-assets.mjs +242 -0
  16. package/skills/ai-asset-generator/generate-sty-icon.mjs +149 -0
  17. package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
  18. package/skills/ai-asset-generator/lib/env.mjs +38 -0
  19. package/skills/ai-asset-generator/lib/kie-client.mjs +88 -0
  20. package/skills/ai-asset-generator/scripts/scaffold-generator.mjs +203 -0
  21. package/skills/ai-build-ai/SKILL.md +124 -0
  22. package/skills/ai-build-ai/docs/agent-teams.md +293 -0
  23. package/skills/ai-build-ai/docs/checkpointing.md +161 -0
  24. package/skills/ai-build-ai/docs/create-agent.md +399 -0
  25. package/skills/ai-build-ai/docs/create-mcp.md +395 -0
  26. package/skills/ai-build-ai/docs/create-skill.md +299 -0
  27. package/skills/ai-build-ai/docs/headless-mode.md +614 -0
  28. package/skills/ai-build-ai/docs/hooks.md +578 -0
  29. package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
  30. package/skills/ai-build-ai/docs/output-styles.md +208 -0
  31. package/skills/ai-build-ai/docs/overview.md +162 -0
  32. package/skills/ai-build-ai/docs/permissions.md +391 -0
  33. package/skills/ai-build-ai/docs/plugins.md +396 -0
  34. package/skills/ai-build-ai/docs/sandbox.md +262 -0
  35. package/skills/ai-build-ai/scripts/load-tutorial.sh +54 -0
  36. package/skills/icon-generator/SKILL.md +270 -0
  37. package/skills/mobile-app-review/SKILL.md +321 -0
  38. package/skills/mobile-app-review/references/apple-review.md +132 -0
  39. package/skills/mobile-app-review/references/google-play-review.md +203 -0
  40. package/skills/mongodb/SKILL.md +667 -0
  41. package/skills/mongodb/references/mongoose-patterns.md +368 -0
  42. package/skills/nestjs-architecture/SKILL.md +1086 -0
  43. package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
  44. package/skills/performance/SKILL.md +509 -0
  45. package/skills/react-fsd-architecture/SKILL.md +693 -0
  46. package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
  47. package/skills/react-query/SKILL.md +685 -0
  48. package/skills/react-query/references/query-patterns.md +365 -0
  49. package/skills/review-code/SKILL.md +321 -0
  50. package/skills/review-code/references/clean-code-principles.md +395 -0
  51. package/skills/review-code/references/frontend-patterns.md +136 -0
  52. package/skills/review-code/references/nestjs-patterns.md +184 -0
  53. package/skills/review-code/scripts/check-module.sh +201 -0
  54. package/skills/review-code/scripts/deep-scan.sh +604 -0
  55. package/skills/review-code/scripts/dep-check.sh +522 -0
  56. package/skills/review-code/scripts/detect-duplicates.sh +466 -0
  57. package/skills/review-code/scripts/format-check.sh +577 -0
  58. package/skills/review-code/scripts/run-review.sh +167 -0
  59. package/skills/review-code/scripts/scan-codebase.sh +152 -0
  60. package/skills/security-scanner/SKILL.md +327 -0
  61. package/skills/security-scanner/references/nestjs-security.md +260 -0
  62. package/skills/security-scanner/references/nextjs-security.md +201 -0
  63. package/skills/security-scanner/references/react-native-security.md +199 -0
  64. package/skills/security-scanner/scripts/security-scan.sh +478 -0
  65. package/skills/ui-ux-pro-max/SKILL.md +377 -0
  66. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  67. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  68. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  69. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  70. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  71. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  72. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  73. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  74. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  75. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  76. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  77. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  78. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  79. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  80. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  81. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  82. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  83. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  84. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  85. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  86. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  87. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  88. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  89. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  90. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  91. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  92. 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