@jgamaraalv/ts-dev-kit 2.2.0 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgamaraalv/ts-dev-kit",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Claude Code plugin: 13 agents + 16 skills for TypeScript fullstack development",
5
5
  "author": "jgamaraalv",
6
6
  "license": "MIT",
@@ -0,0 +1,348 @@
1
+ ---
2
+ name: tanstack-query
3
+ description: |
4
+ TanStack Query v5 (React Query) reference for data fetching, caching,
5
+ and server state management in React. Use when: (1) writing useQuery,
6
+ useMutation, or useInfiniteQuery hooks, (2) setting up QueryClient
7
+ and queryOptions, (3) implementing optimistic updates or cache
8
+ invalidation, (4) configuring SSR/hydration with Next.js App Router
9
+ or Pages Router, (5) testing React Query hooks, (6) working with
10
+ TypeScript types, Suspense, or advanced patterns like dependent
11
+ queries and infinite scroll.
12
+ ---
13
+
14
+ # TanStack Query v5 (React)
15
+
16
+ ## Setup
17
+
18
+ ```tsx
19
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
20
+
21
+ const queryClient = new QueryClient({
22
+ defaultOptions: {
23
+ queries: {
24
+ staleTime: 60 * 1000, // 1 min (default is 0)
25
+ gcTime: 5 * 60 * 1000, // 5 min (default)
26
+ retry: 3, // 3 retries with exponential backoff (default)
27
+ refetchOnWindowFocus: true, // default
28
+ },
29
+ },
30
+ })
31
+
32
+ function App() {
33
+ return (
34
+ <QueryClientProvider client={queryClient}>
35
+ <YourApp />
36
+ </QueryClientProvider>
37
+ )
38
+ }
39
+ ```
40
+
41
+ ## Important Defaults
42
+
43
+ | Default | Value | Notes |
44
+ |---------|-------|-------|
45
+ | `staleTime` | `0` | Cached data is immediately stale; triggers background refetch on mount/focus/reconnect |
46
+ | `gcTime` | `5 min` | Inactive queries garbage collected after 5 minutes |
47
+ | `retry` | `3` (queries) / `0` (mutations) | Queries retry 3x with exponential backoff; mutations do NOT retry |
48
+ | `refetchOnWindowFocus` | `true` | Stale queries refetch when tab regains focus |
49
+ | `refetchOnReconnect` | `true` | Stale queries refetch when network reconnects |
50
+ | `refetchOnMount` | `true` | Stale queries refetch when new instance mounts |
51
+ | `structuralSharing` | `true` | Preserves referential identity if data is structurally equal |
52
+
53
+ **Key recommendation:** Set `staleTime` above 0 to control refetch frequency rather than disabling individual refetch triggers.
54
+
55
+ ## queryOptions — co-locate key + fn
56
+
57
+ Always use `queryOptions` to define query configurations. It enables type inference across `useQuery`, `prefetchQuery`, `getQueryData`, and `setQueryData`.
58
+
59
+ ```tsx
60
+ import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
61
+
62
+ export function todosOptions(filters: TodoFilters) {
63
+ return queryOptions({
64
+ queryKey: ['todos', filters],
65
+ queryFn: () => fetchTodos(filters),
66
+ staleTime: 5 * 1000,
67
+ })
68
+ }
69
+
70
+ // Works everywhere with full type inference:
71
+ useQuery(todosOptions({ status: 'done' }))
72
+ useSuspenseQuery(todosOptions({ status: 'done' }))
73
+ queryClient.prefetchQuery(todosOptions({ status: 'done' }))
74
+ queryClient.setQueryData(todosOptions({ status: 'done' }).queryKey, newData)
75
+ const cached = queryClient.getQueryData(todosOptions({ status: 'done' }).queryKey)
76
+ // ^? TodoItem[] | undefined
77
+ ```
78
+
79
+ For infinite queries, use `infiniteQueryOptions` (same pattern, adds `initialPageParam` and `getNextPageParam`).
80
+
81
+ ## useQuery
82
+
83
+ ```tsx
84
+ const {
85
+ data, // TData | undefined
86
+ error, // TError | null
87
+ status, // 'pending' | 'error' | 'success'
88
+ isPending, // no cached data yet
89
+ isError,
90
+ isSuccess,
91
+ isFetching, // queryFn is running (including background refetch)
92
+ isLoading, // isPending && isFetching (first load only)
93
+ isPlaceholderData, // showing placeholder, not real data
94
+ isStale,
95
+ refetch,
96
+ fetchStatus, // 'fetching' | 'paused' | 'idle'
97
+ } = useQuery({
98
+ queryKey: ['todos', userId], // unique cache key (Array)
99
+ queryFn: () => fetchTodos(userId),
100
+ enabled: !!userId, // disable until userId exists
101
+ staleTime: 60_000,
102
+ select: (data) => data.filter(t => !t.done), // transform/filter
103
+ placeholderData: keepPreviousData, // smooth pagination
104
+ })
105
+ ```
106
+
107
+ **Query states:** `status` tells you "do we have data?"; `fetchStatus` tells you "is the queryFn running?". They are orthogonal — a query can be `pending` + `paused` (no data, no network).
108
+
109
+ ## Query Keys
110
+
111
+ Keys must be Arrays. They are hashed deterministically.
112
+
113
+ ```tsx
114
+ // Object key order does NOT matter — these are equivalent:
115
+ useQuery({ queryKey: ['todos', { status, page }] })
116
+ useQuery({ queryKey: ['todos', { page, status }] })
117
+
118
+ // Array item order DOES matter — these are different:
119
+ useQuery({ queryKey: ['todos', status, page] })
120
+ useQuery({ queryKey: ['todos', page, status] })
121
+ ```
122
+
123
+ **Rule:** If your queryFn depends on a variable, include it in the queryKey. The key acts as a dependency array.
124
+
125
+ ## useMutation
126
+
127
+ ```tsx
128
+ const queryClient = useQueryClient()
129
+
130
+ const mutation = useMutation({
131
+ mutationFn: (newTodo: CreateTodoInput) => api.post('/todos', newTodo),
132
+ onSuccess: (data, variables) => {
133
+ // Invalidate related queries to trigger refetch
134
+ queryClient.invalidateQueries({ queryKey: ['todos'] })
135
+ // Or update cache directly with response data
136
+ queryClient.setQueryData(['todos', data.id], data)
137
+ },
138
+ onError: (error, variables, onMutateResult) => {},
139
+ onSettled: (data, error, variables, onMutateResult) => {},
140
+ })
141
+
142
+ // Trigger:
143
+ mutation.mutate({ title: 'New todo' })
144
+
145
+ // Or with per-call callbacks:
146
+ mutation.mutate(input, { onSuccess: () => navigate('/todos') })
147
+
148
+ // Async variant (returns Promise):
149
+ const data = await mutation.mutateAsync(input)
150
+ ```
151
+
152
+ **Lifecycle:** `onMutate` → `mutationFn` → `onSuccess`/`onError` → `onSettled`. Callbacks returning promises are awaited.
153
+
154
+ **Gotcha:** Per-call `mutate()` callbacks only fire for the *latest* call if mutations overlap. Use `useMutation`-level callbacks for reliable logic.
155
+
156
+ ## Query Invalidation
157
+
158
+ ```tsx
159
+ // Prefix match (default) — invalidates ['todos'] and ['todos', { page: 1 }]
160
+ queryClient.invalidateQueries({ queryKey: ['todos'] })
161
+
162
+ // Exact match only
163
+ queryClient.invalidateQueries({ queryKey: ['todos'], exact: true })
164
+
165
+ // Predicate for fine-grained control
166
+ queryClient.invalidateQueries({
167
+ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
168
+ })
169
+
170
+ // ALL queries
171
+ queryClient.invalidateQueries()
172
+ ```
173
+
174
+ Invalidation marks queries as stale and triggers background refetch for active (rendered) queries.
175
+
176
+ ## Optimistic Updates
177
+
178
+ ### Approach 1: Via the UI (simpler, recommended)
179
+
180
+ Render optimistic state from `variables` directly in JSX:
181
+
182
+ ```tsx
183
+ const { mutate, variables, isPending, isError } = useMutation({
184
+ mutationFn: (text: string) => api.post('/todos', { text }),
185
+ onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
186
+ })
187
+
188
+ // In JSX:
189
+ {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
190
+ {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
191
+ ```
192
+
193
+ Access pending mutations from other components with `useMutationState`:
194
+
195
+ ```tsx
196
+ const pendingTodos = useMutationState<string>({
197
+ filters: { mutationKey: ['addTodo'], status: 'pending' },
198
+ select: (mutation) => mutation.state.variables,
199
+ })
200
+ ```
201
+
202
+ ### Approach 2: Via cache (with rollback)
203
+
204
+ ```tsx
205
+ useMutation({
206
+ mutationFn: updateTodo,
207
+ onMutate: async (newTodo, context) => {
208
+ await context.client.cancelQueries({ queryKey: ['todos'] })
209
+ const previous = context.client.getQueryData(['todos'])
210
+ context.client.setQueryData(['todos'], (old) => [...old, newTodo])
211
+ return { previous }
212
+ },
213
+ onError: (err, newTodo, onMutateResult, context) => {
214
+ context.client.setQueryData(['todos'], onMutateResult.previous)
215
+ },
216
+ onSettled: (data, error, variables, onMutateResult, context) => {
217
+ context.client.invalidateQueries({ queryKey: ['todos'] })
218
+ },
219
+ })
220
+ ```
221
+
222
+ **Always** `cancelQueries` before optimistic update to prevent background refetches from overwriting.
223
+
224
+ ## Infinite Queries
225
+
226
+ ```tsx
227
+ const {
228
+ data, // { pages: T[], pageParams: unknown[] }
229
+ fetchNextPage,
230
+ fetchPreviousPage,
231
+ hasNextPage, // true when getNextPageParam returns non-null/undefined
232
+ hasPreviousPage,
233
+ isFetchingNextPage,
234
+ isFetchingPreviousPage,
235
+ } = useInfiniteQuery({
236
+ queryKey: ['projects'],
237
+ queryFn: ({ pageParam }) => fetchProjects(pageParam),
238
+ initialPageParam: 0, // REQUIRED
239
+ getNextPageParam: (lastPage, allPages) => lastPage.nextCursor ?? undefined,
240
+ maxPages: 3, // optional: cap cached pages
241
+ })
242
+
243
+ // Render all pages:
244
+ {data.pages.map((page, i) => (
245
+ <Fragment key={i}>
246
+ {page.items.map(item => <div key={item.id}>{item.name}</div>)}
247
+ </Fragment>
248
+ ))}
249
+
250
+ // Load more:
251
+ <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
252
+ {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
253
+ </button>
254
+ ```
255
+
256
+ **Gotcha:** `data` is `{ pages, pageParams }`, not flat data. `initialData` and `placeholderData` must match this shape.
257
+
258
+ ## Paginated Queries (keep previous data)
259
+
260
+ ```tsx
261
+ import { keepPreviousData, useQuery } from '@tanstack/react-query'
262
+
263
+ const { data, isPlaceholderData } = useQuery({
264
+ queryKey: ['projects', page],
265
+ queryFn: () => fetchProjects(page),
266
+ placeholderData: keepPreviousData,
267
+ })
268
+
269
+ // Prefetch next page for instant transitions:
270
+ queryClient.prefetchQuery({
271
+ queryKey: ['projects', page + 1],
272
+ queryFn: () => fetchProjects(page + 1),
273
+ })
274
+ ```
275
+
276
+ ## Prefetching
277
+
278
+ ```tsx
279
+ // In event handlers (hover/focus):
280
+ const prefetch = () => queryClient.prefetchQuery(todosOptions())
281
+ <button onMouseEnter={prefetch} onFocus={prefetch} onClick={handleClick}>Show</button>
282
+
283
+ // In components (avoid Suspense waterfalls):
284
+ function Layout({ id }: { id: string }) {
285
+ usePrefetchQuery(commentsOptions(id)) // starts fetch immediately
286
+ return (
287
+ <Suspense fallback="Loading...">
288
+ <Article id={id} />
289
+ </Suspense>
290
+ )
291
+ }
292
+
293
+ // Prefetch infinite queries:
294
+ queryClient.prefetchInfiniteQuery({
295
+ ...projectsInfiniteOptions(),
296
+ pages: 3, // prefetch first 3 pages
297
+ })
298
+ ```
299
+
300
+ ## Dependent Queries
301
+
302
+ ```tsx
303
+ const { data: user } = useQuery({
304
+ queryKey: ['user', email],
305
+ queryFn: () => getUserByEmail(email),
306
+ })
307
+
308
+ const { data: projects } = useQuery({
309
+ queryKey: ['projects', user?.id],
310
+ queryFn: () => getProjectsByUser(user!.id),
311
+ enabled: !!user?.id, // waits for user query
312
+ })
313
+ ```
314
+
315
+ **Type-safe disabling with skipToken:**
316
+
317
+ ```tsx
318
+ import { skipToken } from '@tanstack/react-query'
319
+
320
+ const { data } = useQuery({
321
+ queryKey: ['projects', userId],
322
+ queryFn: userId ? () => getProjects(userId) : skipToken,
323
+ })
324
+ ```
325
+
326
+ `skipToken` prevents `refetch()` from working — use `enabled: false` if you need manual refetch.
327
+
328
+ ## setQueryData — immutability
329
+
330
+ ```tsx
331
+ // WRONG — mutating cache in place
332
+ queryClient.setQueryData(['todo', id], (old) => {
333
+ if (old) old.title = 'new' // DO NOT DO THIS
334
+ return old
335
+ })
336
+
337
+ // CORRECT — return new object
338
+ queryClient.setQueryData(['todo', id], (old) =>
339
+ old ? { ...old, title: 'new' } : old
340
+ )
341
+ ```
342
+
343
+ ## Further Reference
344
+
345
+ - **Full API signatures** (useQuery, useMutation, useInfiniteQuery, QueryClient): See [references/api-reference.md](references/api-reference.md)
346
+ - **SSR & Next.js** (hydration, App Router, streaming): See [references/ssr-nextjs.md](references/ssr-nextjs.md)
347
+ - **Testing** (renderHook, mocking, setup): See [references/testing.md](references/testing.md)
348
+ - **Advanced patterns** (TypeScript, Suspense, waterfalls, network modes): See [references/advanced-patterns.md](references/advanced-patterns.md)