@jgamaraalv/ts-dev-kit 2.1.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/tanstack-query/SKILL.md +348 -0
- package/skills/tanstack-query/references/advanced-patterns.md +376 -0
- package/skills/tanstack-query/references/api-reference.md +297 -0
- package/skills/tanstack-query/references/ssr-nextjs.md +272 -0
- package/skills/tanstack-query/references/testing.md +175 -0
- package/skills/task/SKILL.md +41 -40
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# TanStack Query v5 — Advanced Patterns
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [TypeScript](#typescript)
|
|
5
|
+
- [Suspense](#suspense)
|
|
6
|
+
- [Dependent Queries](#dependent-queries)
|
|
7
|
+
- [Disabling Queries](#disabling-queries)
|
|
8
|
+
- [Network Modes](#network-modes)
|
|
9
|
+
- [Request Waterfalls](#request-waterfalls)
|
|
10
|
+
- [Default Query Function](#default-query-function)
|
|
11
|
+
- [Window Focus Refetching](#window-focus-refetching)
|
|
12
|
+
- [Placeholder Data](#placeholder-data)
|
|
13
|
+
|
|
14
|
+
## TypeScript
|
|
15
|
+
|
|
16
|
+
Requires TypeScript v4.7+. Type changes ship as **patch** semver.
|
|
17
|
+
|
|
18
|
+
### Type Inference
|
|
19
|
+
|
|
20
|
+
Types flow from `queryFn` automatically — do not pass generics unless necessary:
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
// GOOD — types inferred
|
|
24
|
+
const { data } = useQuery({
|
|
25
|
+
queryKey: ['todos'],
|
|
26
|
+
queryFn: () => fetchTodos(), // returns Promise<Todo[]>
|
|
27
|
+
})
|
|
28
|
+
// data: Todo[] | undefined
|
|
29
|
+
|
|
30
|
+
// BAD — explicit generics break inference for other params
|
|
31
|
+
const { data } = useQuery<Todo[], Error>({ ... })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Type Narrowing (discriminated union)
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const { data, isSuccess, error, isError } = useQuery({ ... })
|
|
38
|
+
|
|
39
|
+
if (isSuccess) {
|
|
40
|
+
data // Todo[] (not undefined)
|
|
41
|
+
}
|
|
42
|
+
if (isError) {
|
|
43
|
+
error // Error (not null)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Custom Error Types
|
|
48
|
+
|
|
49
|
+
Prefer type narrowing over generics:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
if (axios.isAxiosError(error)) {
|
|
53
|
+
error // AxiosError
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Register Interface — global type overrides
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
declare module '@tanstack/react-query' {
|
|
61
|
+
interface Register {
|
|
62
|
+
defaultError: AxiosError // all errors typed as AxiosError
|
|
63
|
+
queryMeta: { auth?: boolean } // must extend Record<string, unknown>
|
|
64
|
+
mutationMeta: { auth?: boolean }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### queryOptions / mutationOptions for type flow
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { queryOptions, mutationOptions } from '@tanstack/react-query'
|
|
73
|
+
|
|
74
|
+
const todosOpts = (filters: Filters) => queryOptions({
|
|
75
|
+
queryKey: ['todos', filters],
|
|
76
|
+
queryFn: () => fetchTodos(filters),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Types flow to getQueryData:
|
|
80
|
+
const data = queryClient.getQueryData(todosOpts({ status: 'done' }).queryKey)
|
|
81
|
+
// ^? Todo[] | undefined
|
|
82
|
+
|
|
83
|
+
const addTodoOpts = () => mutationOptions({
|
|
84
|
+
mutationKey: ['addTodo'],
|
|
85
|
+
mutationFn: (input: CreateTodo) => createTodo(input),
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### skipToken — type-safe conditional queries
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { skipToken, useQuery } from '@tanstack/react-query'
|
|
93
|
+
|
|
94
|
+
const { data } = useQuery({
|
|
95
|
+
queryKey: ['user', userId],
|
|
96
|
+
queryFn: userId ? () => fetchUser(userId) : skipToken,
|
|
97
|
+
})
|
|
98
|
+
// queryFn type is correctly inferred without worrying about undefined userId
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Suspense
|
|
102
|
+
|
|
103
|
+
### Dedicated Hooks
|
|
104
|
+
|
|
105
|
+
| Hook | Purpose |
|
|
106
|
+
|------|---------|
|
|
107
|
+
| `useSuspenseQuery` | Suspends until data ready; `data` guaranteed defined |
|
|
108
|
+
| `useSuspenseInfiniteQuery` | Same for infinite queries |
|
|
109
|
+
| `useSuspenseQueries` | Parallel suspense queries (avoids serial waterfall) |
|
|
110
|
+
|
|
111
|
+
### Key differences from useQuery
|
|
112
|
+
|
|
113
|
+
- No `enabled` option — cannot conditionally disable
|
|
114
|
+
- No `placeholderData` — use `startTransition` to avoid fallback on key change
|
|
115
|
+
- `data` is always defined (never `undefined`)
|
|
116
|
+
- Multiple suspense queries in same component fetch **serially** — use `useSuspenseQueries` for parallel
|
|
117
|
+
|
|
118
|
+
### Parallel suspense queries
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// WRONG — serial (Article suspends, then Comments fetches after)
|
|
122
|
+
function Page({ id }: { id: string }) {
|
|
123
|
+
const { data: article } = useSuspenseQuery(articleOptions(id))
|
|
124
|
+
const { data: comments } = useSuspenseQuery(commentsOptions(id))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// CORRECT — parallel
|
|
128
|
+
function Page({ id }: { id: string }) {
|
|
129
|
+
const [{ data: article }, { data: comments }] = useSuspenseQueries({
|
|
130
|
+
queries: [articleOptions(id), commentsOptions(id)],
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Error boundary reset
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import { QueryErrorResetBoundary } from '@tanstack/react-query'
|
|
139
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
140
|
+
|
|
141
|
+
function App() {
|
|
142
|
+
return (
|
|
143
|
+
<QueryErrorResetBoundary>
|
|
144
|
+
{({ reset }) => (
|
|
145
|
+
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
|
|
146
|
+
<div>
|
|
147
|
+
<p>Something went wrong</p>
|
|
148
|
+
<button onClick={() => resetErrorBoundary()}>Try again</button>
|
|
149
|
+
</div>
|
|
150
|
+
)}>
|
|
151
|
+
<Page />
|
|
152
|
+
</ErrorBoundary>
|
|
153
|
+
)}
|
|
154
|
+
</QueryErrorResetBoundary>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### throwOnError default for suspense
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
// Only throws when there's NO cached data
|
|
163
|
+
throwOnError: (error, query) => typeof query.state.data === 'undefined'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
To throw all errors: manually throw when `error && !isFetching`.
|
|
167
|
+
|
|
168
|
+
## Dependent Queries
|
|
169
|
+
|
|
170
|
+
Use `enabled` to chain queries:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
const { data: user } = useQuery({
|
|
174
|
+
queryKey: ['user', email],
|
|
175
|
+
queryFn: () => getUserByEmail(email),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const { data: projects } = useQuery({
|
|
179
|
+
queryKey: ['projects', user?.id],
|
|
180
|
+
queryFn: () => getProjectsByUser(user!.id),
|
|
181
|
+
enabled: !!user?.id,
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Dynamic parallel dependent queries:**
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
const { data: userIds } = useQuery({
|
|
189
|
+
queryKey: ['users'],
|
|
190
|
+
queryFn: getUsersData,
|
|
191
|
+
select: (users) => users.map(u => u.id),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const usersMessages = useQueries({
|
|
195
|
+
queries: userIds
|
|
196
|
+
? userIds.map((id) => ({
|
|
197
|
+
queryKey: ['messages', id],
|
|
198
|
+
queryFn: () => getMessagesByUser(id),
|
|
199
|
+
}))
|
|
200
|
+
: [],
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Disabling Queries
|
|
205
|
+
|
|
206
|
+
### enabled: false
|
|
207
|
+
|
|
208
|
+
- No auto-fetch on mount, no background refetch
|
|
209
|
+
- Ignores `invalidateQueries` and `refetchQueries`
|
|
210
|
+
- Manual `refetch()` still works
|
|
211
|
+
- Without cache: starts `status: 'pending'`, `fetchStatus: 'idle'`
|
|
212
|
+
|
|
213
|
+
### skipToken (preferred for TypeScript)
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const { data } = useQuery({
|
|
217
|
+
queryKey: ['todos', filter],
|
|
218
|
+
queryFn: filter ? () => fetchTodos(filter) : skipToken,
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**skipToken does NOT support `refetch()`** — throws "Missing queryFn". Use `enabled: false` if manual refetch is needed.
|
|
223
|
+
|
|
224
|
+
### Lazy queries (preferred pattern)
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
const [enabled, setEnabled] = useState(false)
|
|
228
|
+
const { data } = useQuery({
|
|
229
|
+
queryKey: ['todos'],
|
|
230
|
+
queryFn: fetchTodos,
|
|
231
|
+
enabled,
|
|
232
|
+
})
|
|
233
|
+
<button onClick={() => setEnabled(true)}>Load</button>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### isLoading vs isPending
|
|
237
|
+
|
|
238
|
+
- `isPending` = no cached data (regardless of fetch status)
|
|
239
|
+
- `isLoading` = `isPending && isFetching` (first actual load)
|
|
240
|
+
|
|
241
|
+
Use `isLoading` to distinguish "first load" from "disabled/idle".
|
|
242
|
+
|
|
243
|
+
## Network Modes
|
|
244
|
+
|
|
245
|
+
`networkMode: 'online' | 'always' | 'offlineFirst'` (default: `'online'`)
|
|
246
|
+
|
|
247
|
+
| Mode | Behavior | Use Case |
|
|
248
|
+
|------|----------|----------|
|
|
249
|
+
| `online` | Only fetches with network; pauses retries offline | Default for most APIs |
|
|
250
|
+
| `always` | Ignores online/offline; never pauses | AsyncStorage, local data, `Promise.resolve` |
|
|
251
|
+
| `offlineFirst` | Tries once, pauses retries if offline | Service worker / HTTP cache (PWAs) |
|
|
252
|
+
|
|
253
|
+
**Key:** With `online` mode, check both `status` and `fetchStatus` before showing spinners — a query can be `pending` + `paused` (no data, no network).
|
|
254
|
+
|
|
255
|
+
## Request Waterfalls
|
|
256
|
+
|
|
257
|
+
Each waterfall step = at least one server roundtrip. At 250ms latency (3G), 4 steps = 1000ms overhead.
|
|
258
|
+
|
|
259
|
+
### Three types
|
|
260
|
+
|
|
261
|
+
**1. Serial queries in one component:**
|
|
262
|
+
```tsx
|
|
263
|
+
// BAD — waterfall
|
|
264
|
+
const { data: user } = useQuery(userOptions(email))
|
|
265
|
+
const { data: projects } = useQuery({ ...projectsOptions(user?.id), enabled: !!user?.id })
|
|
266
|
+
|
|
267
|
+
// FIX — restructure API to avoid dependency
|
|
268
|
+
const { data: projects } = useQuery(projectsByEmailOptions(email))
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**2. Nested component waterfalls:**
|
|
272
|
+
Parent suspends → child mounts → child fetches. Fix: hoist child query up or prefetch at route level.
|
|
273
|
+
|
|
274
|
+
**3. Code splitting + queries:**
|
|
275
|
+
`Markup → JS → Lazy chunk → Query` = quadruple waterfall. Fix: prefetch at route level.
|
|
276
|
+
|
|
277
|
+
### Fix with useSuspenseQueries
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
// BAD — serial (each suspends before next mounts)
|
|
281
|
+
const { data: a } = useSuspenseQuery(optionsA)
|
|
282
|
+
const { data: b } = useSuspenseQuery(optionsB)
|
|
283
|
+
|
|
284
|
+
// GOOD — parallel
|
|
285
|
+
const [{ data: a }, { data: b }] = useSuspenseQueries({
|
|
286
|
+
queries: [optionsA, optionsB],
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Fix with prefetching
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
// In route loader or parent component:
|
|
294
|
+
queryClient.prefetchQuery(articleOptions(id))
|
|
295
|
+
queryClient.prefetchQuery(commentsOptions(id))
|
|
296
|
+
// Both start fetching immediately, no waterfall
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Default Query Function
|
|
300
|
+
|
|
301
|
+
Share one `queryFn` across all queries:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
const defaultQueryFn = async ({ queryKey }: { queryKey: QueryKey }) => {
|
|
305
|
+
const { data } = await axios.get(`https://api.example.com${queryKey[0]}`)
|
|
306
|
+
return data
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const queryClient = new QueryClient({
|
|
310
|
+
defaultOptions: { queries: { queryFn: defaultQueryFn } },
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Usage — no queryFn needed:
|
|
314
|
+
useQuery({ queryKey: ['/posts'] })
|
|
315
|
+
useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId })
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Per-query `queryFn` overrides the default.
|
|
319
|
+
|
|
320
|
+
## Window Focus Refetching
|
|
321
|
+
|
|
322
|
+
Enabled by default. Stale queries refetch when the browser tab regains focus.
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
// Disable globally:
|
|
326
|
+
new QueryClient({
|
|
327
|
+
defaultOptions: { queries: { refetchOnWindowFocus: false } },
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Disable per query:
|
|
331
|
+
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false })
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### React Native
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { AppState, Platform } from 'react-native'
|
|
338
|
+
import { focusManager } from '@tanstack/react-query'
|
|
339
|
+
|
|
340
|
+
function onAppStateChange(status: AppStateStatus) {
|
|
341
|
+
if (Platform.OS !== 'web') {
|
|
342
|
+
focusManager.setFocused(status === 'active')
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
const sub = AppState.addEventListener('change', onAppStateChange)
|
|
348
|
+
return () => sub.remove()
|
|
349
|
+
}, [])
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Placeholder Data
|
|
353
|
+
|
|
354
|
+
Temporary display data that is NOT persisted to cache (unlike `initialData`).
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
// Static placeholder
|
|
358
|
+
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, placeholderData: [] })
|
|
359
|
+
|
|
360
|
+
// Keep previous data during key changes (pagination)
|
|
361
|
+
useQuery({
|
|
362
|
+
queryKey: ['todos', id],
|
|
363
|
+
queryFn: () => fetchTodo(id),
|
|
364
|
+
placeholderData: (previousData) => previousData,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
// From another query's cache (list → detail preview)
|
|
368
|
+
useQuery({
|
|
369
|
+
queryKey: ['todo', todoId],
|
|
370
|
+
queryFn: () => fetchTodo(todoId),
|
|
371
|
+
placeholderData: () =>
|
|
372
|
+
queryClient.getQueryData<Todo[]>(['todos'])?.find(t => t.id === todoId),
|
|
373
|
+
})
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`isPlaceholderData` is `true` when showing placeholder data.
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# TanStack Query v5 — API Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [useQuery Options](#usequery-options)
|
|
5
|
+
- [useQuery Returns](#usequery-returns)
|
|
6
|
+
- [useMutation Options](#usemutation-options)
|
|
7
|
+
- [useMutation Returns](#usemutation-returns)
|
|
8
|
+
- [useInfiniteQuery Options](#useinfinitequery-options)
|
|
9
|
+
- [useInfiniteQuery Returns](#useinfinitequery-returns)
|
|
10
|
+
- [QueryClient Methods](#queryclient-methods)
|
|
11
|
+
- [QueryCache](#querycache)
|
|
12
|
+
- [MutationCache](#mutationcache)
|
|
13
|
+
- [queryOptions / infiniteQueryOptions](#queryoptions--infinitequeryoptions)
|
|
14
|
+
|
|
15
|
+
## useQuery Options
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const result = useQuery(options, queryClient?)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Option | Type | Default |
|
|
22
|
+
|--------|------|---------|
|
|
23
|
+
| `queryKey` | `unknown[]` | **required** |
|
|
24
|
+
| `queryFn` | `(context: QueryFunctionContext) => Promise<TData>` | **required** (unless default set) |
|
|
25
|
+
| `enabled` | `boolean \| (query: Query) => boolean` | `true` |
|
|
26
|
+
| `staleTime` | `number \| 'static' \| ((query) => number \| 'static')` | `0` |
|
|
27
|
+
| `gcTime` | `number \| Infinity` | `300000` (5 min) / `Infinity` on server |
|
|
28
|
+
| `retry` | `boolean \| number \| (failureCount, error) => boolean` | `3` client / `0` server |
|
|
29
|
+
| `retryDelay` | `number \| (attempt, error) => number` | exponential backoff |
|
|
30
|
+
| `retryOnMount` | `boolean` | `true` |
|
|
31
|
+
| `refetchInterval` | `number \| false \| ((query) => number \| false)` | — |
|
|
32
|
+
| `refetchIntervalInBackground` | `boolean` | `false` |
|
|
33
|
+
| `refetchOnMount` | `boolean \| 'always' \| ((query) => boolean \| 'always')` | `true` |
|
|
34
|
+
| `refetchOnWindowFocus` | `boolean \| 'always' \| ((query) => boolean \| 'always')` | `true` |
|
|
35
|
+
| `refetchOnReconnect` | `boolean \| 'always' \| ((query) => boolean \| 'always')` | `true` |
|
|
36
|
+
| `networkMode` | `'online' \| 'always' \| 'offlineFirst'` | `'online'` |
|
|
37
|
+
| `select` | `(data: TData) => unknown` | — |
|
|
38
|
+
| `initialData` | `TData \| () => TData` | — |
|
|
39
|
+
| `initialDataUpdatedAt` | `number \| (() => number)` | — |
|
|
40
|
+
| `placeholderData` | `TData \| (previousValue, previousQuery) => TData` | — |
|
|
41
|
+
| `notifyOnChangeProps` | `string[] \| 'all'` | tracked access |
|
|
42
|
+
| `structuralSharing` | `boolean \| (oldData, newData) => unknown` | `true` |
|
|
43
|
+
| `throwOnError` | `boolean \| (error, query) => boolean` | — |
|
|
44
|
+
| `meta` | `Record<string, unknown>` | — |
|
|
45
|
+
| `subscribed` | `boolean` | `true` |
|
|
46
|
+
|
|
47
|
+
**QueryFunctionContext** passed to `queryFn`:
|
|
48
|
+
- `queryKey` — the query key array
|
|
49
|
+
- `client` — the QueryClient instance
|
|
50
|
+
- `signal` — AbortSignal for cancellation
|
|
51
|
+
- `meta` — optional metadata from the query option
|
|
52
|
+
|
|
53
|
+
**Key notes:**
|
|
54
|
+
- `queryFn` must resolve data; data **cannot be `undefined`**
|
|
55
|
+
- `initialData` persists to cache; `placeholderData` does not
|
|
56
|
+
- `select` only re-runs when data or function reference changes — wrap in `useCallback` if inline
|
|
57
|
+
- `staleTime: 'static'` — data is never stale, even `refetchOnWindowFocus: 'always'` is blocked
|
|
58
|
+
|
|
59
|
+
## useQuery Returns
|
|
60
|
+
|
|
61
|
+
| Property | Type | Notes |
|
|
62
|
+
|----------|------|-------|
|
|
63
|
+
| `data` | `TData \| undefined` | |
|
|
64
|
+
| `error` | `TError \| null` | |
|
|
65
|
+
| `status` | `'pending' \| 'error' \| 'success'` | |
|
|
66
|
+
| `isPending` | `boolean` | No cached data |
|
|
67
|
+
| `isSuccess` | `boolean` | |
|
|
68
|
+
| `isError` | `boolean` | |
|
|
69
|
+
| `isLoading` | `boolean` | `isPending && isFetching` (first load) |
|
|
70
|
+
| `isLoadingError` | `boolean` | Failed on first fetch |
|
|
71
|
+
| `isRefetchError` | `boolean` | Failed while refetching |
|
|
72
|
+
| `fetchStatus` | `'fetching' \| 'paused' \| 'idle'` | |
|
|
73
|
+
| `isFetching` | `boolean` | queryFn running |
|
|
74
|
+
| `isPaused` | `boolean` | Wants to fetch but offline |
|
|
75
|
+
| `isRefetching` | `boolean` | `isFetching && !isPending` |
|
|
76
|
+
| `isStale` | `boolean` | |
|
|
77
|
+
| `isPlaceholderData` | `boolean` | |
|
|
78
|
+
| `isFetched` | `boolean` | |
|
|
79
|
+
| `isFetchedAfterMount` | `boolean` | |
|
|
80
|
+
| `dataUpdatedAt` | `number` | Timestamp |
|
|
81
|
+
| `errorUpdatedAt` | `number` | |
|
|
82
|
+
| `failureCount` | `number` | Resets to 0 on success |
|
|
83
|
+
| `failureReason` | `TError \| null` | |
|
|
84
|
+
| `errorUpdateCount` | `number` | Total errors ever |
|
|
85
|
+
| `isEnabled` | `boolean` | |
|
|
86
|
+
| `refetch` | `(opts?) => Promise<UseQueryResult>` | `cancelRefetch` defaults to `true` |
|
|
87
|
+
|
|
88
|
+
## useMutation Options
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const result = useMutation(options, queryClient?)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Option | Type | Default |
|
|
95
|
+
|--------|------|---------|
|
|
96
|
+
| `mutationFn` | `(variables, context: MutationFunctionContext) => Promise<TData>` | **required** |
|
|
97
|
+
| `mutationKey` | `unknown[]` | — |
|
|
98
|
+
| `gcTime` | `number \| Infinity` | — |
|
|
99
|
+
| `retry` | `boolean \| number \| (failureCount, error) => boolean` | `0` |
|
|
100
|
+
| `retryDelay` | `number \| (attempt, error) => number` | — |
|
|
101
|
+
| `networkMode` | `'online' \| 'always' \| 'offlineFirst'` | `'online'` |
|
|
102
|
+
| `onMutate` | `(variables, context) => Promise<TOnMutateResult> \| TOnMutateResult` | — |
|
|
103
|
+
| `onSuccess` | `(data, variables, onMutateResult, context) => Promise \| void` | — |
|
|
104
|
+
| `onError` | `(error, variables, onMutateResult, context) => Promise \| void` | — |
|
|
105
|
+
| `onSettled` | `(data, error, variables, onMutateResult, context) => Promise \| void` | — |
|
|
106
|
+
| `scope` | `{ id: string }` | unique (parallel) |
|
|
107
|
+
| `throwOnError` | `boolean \| (error) => boolean` | — |
|
|
108
|
+
| `meta` | `Record<string, unknown>` | — |
|
|
109
|
+
|
|
110
|
+
**Callback context:** `onMutate` return value is passed as `onMutateResult` to `onSuccess`/`onError`/`onSettled`. The `context` object provides `context.client` (the QueryClient).
|
|
111
|
+
|
|
112
|
+
**Scope:** Mutations with the same `scope.id` run **serially** (queued), not in parallel.
|
|
113
|
+
|
|
114
|
+
## useMutation Returns
|
|
115
|
+
|
|
116
|
+
| Property | Type | Notes |
|
|
117
|
+
|----------|------|-------|
|
|
118
|
+
| `mutate` | `(variables, { onSuccess?, onError?, onSettled? }) => void` | Fire and forget |
|
|
119
|
+
| `mutateAsync` | `(variables, opts?) => Promise<TData>` | Returns promise |
|
|
120
|
+
| `status` | `'idle' \| 'pending' \| 'error' \| 'success'` | |
|
|
121
|
+
| `isIdle`, `isPending`, `isSuccess`, `isError` | `boolean` | |
|
|
122
|
+
| `isPaused` | `boolean` | |
|
|
123
|
+
| `data` | `TData \| undefined` | |
|
|
124
|
+
| `error` | `TError \| null` | |
|
|
125
|
+
| `variables` | `TVariables \| undefined` | Last call's variables |
|
|
126
|
+
| `reset` | `() => void` | Clear mutation state |
|
|
127
|
+
| `submittedAt` | `number` | |
|
|
128
|
+
| `failureCount` | `number` | |
|
|
129
|
+
| `failureReason` | `TError \| null` | |
|
|
130
|
+
|
|
131
|
+
## useInfiniteQuery Options
|
|
132
|
+
|
|
133
|
+
Inherits all `useQuery` options plus:
|
|
134
|
+
|
|
135
|
+
| Option | Type | Default |
|
|
136
|
+
|--------|------|---------|
|
|
137
|
+
| `initialPageParam` | `TPageParam` | **required** |
|
|
138
|
+
| `getNextPageParam` | `(lastPage, allPages, lastPageParam, allPageParams) => TPageParam \| undefined \| null` | **required** |
|
|
139
|
+
| `getPreviousPageParam` | `(firstPage, allPages, firstPageParam, allPageParams) => TPageParam \| undefined \| null` | — |
|
|
140
|
+
| `maxPages` | `number` | `undefined` (unlimited) |
|
|
141
|
+
|
|
142
|
+
Return `undefined`/`null` from page param getters to signal no more pages. When `maxPages` is set, both `getNextPageParam` and `getPreviousPageParam` must be defined.
|
|
143
|
+
|
|
144
|
+
## useInfiniteQuery Returns
|
|
145
|
+
|
|
146
|
+
Inherits all `useQuery` returns plus:
|
|
147
|
+
|
|
148
|
+
| Property | Type | Notes |
|
|
149
|
+
|----------|------|-------|
|
|
150
|
+
| `data.pages` | `TData[]` | Array of page results |
|
|
151
|
+
| `data.pageParams` | `unknown[]` | Array of page params used |
|
|
152
|
+
| `fetchNextPage` | `(opts?) => Promise` | |
|
|
153
|
+
| `fetchPreviousPage` | `(opts?) => Promise` | |
|
|
154
|
+
| `hasNextPage` | `boolean` | |
|
|
155
|
+
| `hasPreviousPage` | `boolean` | |
|
|
156
|
+
| `isFetchingNextPage` | `boolean` | |
|
|
157
|
+
| `isFetchingPreviousPage` | `boolean` | |
|
|
158
|
+
| `isFetchNextPageError` | `boolean` | |
|
|
159
|
+
| `isFetchPreviousPageError` | `boolean` | |
|
|
160
|
+
|
|
161
|
+
## QueryClient Methods
|
|
162
|
+
|
|
163
|
+
### Constructor
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const queryClient = new QueryClient({
|
|
167
|
+
queryCache?: QueryCache,
|
|
168
|
+
mutationCache?: MutationCache,
|
|
169
|
+
defaultOptions?: {
|
|
170
|
+
queries?: QueryOptions,
|
|
171
|
+
mutations?: MutationOptions,
|
|
172
|
+
hydrate?: HydrateOptions,
|
|
173
|
+
dehydrate?: DehydrateOptions,
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Data Access (synchronous)
|
|
179
|
+
|
|
180
|
+
| Method | Signature | Notes |
|
|
181
|
+
|--------|-----------|-------|
|
|
182
|
+
| `getQueryData` | `(queryKey) => TData \| undefined` | |
|
|
183
|
+
| `getQueriesData` | `(filters) => [QueryKey, TData \| undefined][]` | |
|
|
184
|
+
| `getQueryState` | `(queryKey) => QueryState \| undefined` | |
|
|
185
|
+
| `setQueryData` | `(queryKey, updater: TData \| (old => TData)) => TData \| undefined` | **Must be immutable** |
|
|
186
|
+
| `setQueriesData` | `(filters, updater) => [QueryKey, TData \| undefined][]` | Only updates existing entries |
|
|
187
|
+
|
|
188
|
+
### Fetching (async)
|
|
189
|
+
|
|
190
|
+
| Method | Signature | Notes |
|
|
191
|
+
|--------|-----------|-------|
|
|
192
|
+
| `fetchQuery` | `(options) => Promise<TData>` | Returns cached data if fresh; throws on error |
|
|
193
|
+
| `prefetchQuery` | `(options) => Promise<void>` | Never throws; never returns data |
|
|
194
|
+
| `ensureQueryData` | `(options) => Promise<TData>` | Only fetches if not cached; `revalidateIfStale` option |
|
|
195
|
+
| `fetchInfiniteQuery` | `(options) => Promise<InfiniteData>` | |
|
|
196
|
+
| `prefetchInfiniteQuery` | `(options) => Promise<void>` | |
|
|
197
|
+
| `ensureInfiniteQueryData` | `(options) => Promise<InfiniteData>` | |
|
|
198
|
+
|
|
199
|
+
### Cache Management
|
|
200
|
+
|
|
201
|
+
| Method | Signature | Notes |
|
|
202
|
+
|--------|-----------|-------|
|
|
203
|
+
| `invalidateQueries` | `(filters?, options?) => Promise<void>` | Marks stale + refetches active |
|
|
204
|
+
| `refetchQueries` | `(filters?, options?) => Promise<void>` | |
|
|
205
|
+
| `cancelQueries` | `(filters?) => Promise<void>` | |
|
|
206
|
+
| `removeQueries` | `(filters?) => void` | |
|
|
207
|
+
| `resetQueries` | `(filters?, options?) => Promise<void>` | Resets to initial state |
|
|
208
|
+
| `clear` | `() => void` | Clears all caches |
|
|
209
|
+
|
|
210
|
+
### invalidateQueries filters
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
queryClient.invalidateQueries({
|
|
214
|
+
queryKey?: QueryKey, // prefix match by default
|
|
215
|
+
exact?: boolean, // exact key match
|
|
216
|
+
refetchType?: 'active' | 'inactive' | 'all' | 'none', // default: 'active'
|
|
217
|
+
predicate?: (query: Query) => boolean,
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Defaults
|
|
222
|
+
|
|
223
|
+
| Method | Signature |
|
|
224
|
+
|--------|-----------|
|
|
225
|
+
| `setQueryDefaults` | `(queryKey, options) => void` |
|
|
226
|
+
| `getQueryDefaults` | `(queryKey) => QueryOptions` |
|
|
227
|
+
| `setMutationDefaults` | `(mutationKey, options) => void` |
|
|
228
|
+
| `getMutationDefaults` | `(mutationKey) => MutationOptions` |
|
|
229
|
+
| `setDefaultOptions` | `(options) => void` |
|
|
230
|
+
|
|
231
|
+
`setQueryDefaults` order matters: register from most generic to least generic key.
|
|
232
|
+
|
|
233
|
+
### Status
|
|
234
|
+
|
|
235
|
+
| Method | Returns |
|
|
236
|
+
|--------|---------|
|
|
237
|
+
| `isFetching(filters?)` | `number` — count of fetching queries |
|
|
238
|
+
| `isMutating(filters?)` | `number` — count of in-flight mutations |
|
|
239
|
+
|
|
240
|
+
## QueryCache
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
const queryCache = new QueryCache({
|
|
244
|
+
onError?: (error, query) => void,
|
|
245
|
+
onSuccess?: (data, query) => void,
|
|
246
|
+
onSettled?: (data, error, query) => void,
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
| Method | Returns |
|
|
251
|
+
|--------|---------|
|
|
252
|
+
| `queryCache.find(filters)` | `Query \| undefined` |
|
|
253
|
+
| `queryCache.findAll(filters?)` | `Query[]` |
|
|
254
|
+
| `queryCache.subscribe(callback)` | `() => void` (unsubscribe) |
|
|
255
|
+
| `queryCache.clear()` | `void` |
|
|
256
|
+
|
|
257
|
+
## MutationCache
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
const mutationCache = new MutationCache({
|
|
261
|
+
onMutate?: (variables, mutation, context) => void,
|
|
262
|
+
onError?: (error, variables, onMutateResult, mutation, context) => void,
|
|
263
|
+
onSuccess?: (data, variables, onMutateResult, mutation, context) => void,
|
|
264
|
+
onSettled?: (data, error, variables, onMutateResult, mutation, context) => void,
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Global MutationCache callbacks **always fire** and cannot be overridden by individual mutations. If callbacks return a Promise, it is awaited.
|
|
269
|
+
|
|
270
|
+
| Method | Returns |
|
|
271
|
+
|--------|---------|
|
|
272
|
+
| `mutationCache.getAll()` | `Mutation[]` |
|
|
273
|
+
| `mutationCache.subscribe(callback)` | `() => void` |
|
|
274
|
+
| `mutationCache.clear()` | `void` |
|
|
275
|
+
|
|
276
|
+
## queryOptions / infiniteQueryOptions
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
|
|
280
|
+
|
|
281
|
+
// queryOptions — preserves type inference across useQuery, prefetchQuery, getQueryData
|
|
282
|
+
const opts = queryOptions({
|
|
283
|
+
queryKey: ['groups', id],
|
|
284
|
+
queryFn: () => fetchGroups(id),
|
|
285
|
+
staleTime: 5000,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// infiniteQueryOptions — same for infinite queries
|
|
289
|
+
const infOpts = infiniteQueryOptions({
|
|
290
|
+
queryKey: ['projects'],
|
|
291
|
+
queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
|
292
|
+
initialPageParam: 0,
|
|
293
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Without `queryOptions`, `getQueryData` returns `unknown`. With it, types flow automatically.
|