0
# Configuration Helpers
1
2
This document covers type-safe configuration helpers for creating enhanced query and mutation options in @tanstack/react-query.
3
4
## Overview
5
6
Configuration helpers provide type-safe ways to create query and mutation options with enhanced TypeScript inference. These helpers are particularly useful for creating reusable query configurations and ensuring proper type safety across your application.
7
8
## Query Configuration
9
10
### queryOptions
11
12
Creates type-safe query options with enhanced TypeScript data tagging for better type inference.
13
14
```typescript { .api }
15
function queryOptions<
16
TQueryFnData = unknown,
17
TError = DefaultError,
18
TData = TQueryFnData,
19
TQueryKey extends QueryKey = QueryKey
20
>(
21
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>
22
): UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
23
queryKey: TQueryKey
24
}
25
```
26
27
**Parameters:**
28
- `options`: `UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>` - Query configuration object
29
30
**Returns:** Enhanced query options with improved TypeScript inference
31
32
**Key Features:**
33
- Enhanced type inference for query data
34
- Type tagging for better IDE support
35
- Supports all standard query options
36
- Multiple overloads for different initial data scenarios
37
38
**Example:**
39
```typescript
40
import { queryOptions, useQuery } from '@tanstack/react-query'
41
42
// Define reusable query options
43
const userQueryOptions = (userId: string) =>
44
queryOptions({
45
queryKey: ['users', userId],
46
queryFn: async () => {
47
const response = await fetch(`/api/users/${userId}`)
48
return response.json() as Promise<User>
49
},
50
staleTime: 5 * 60 * 1000, // 5 minutes
51
gcTime: 10 * 60 * 1000, // 10 minutes
52
})
53
54
// Use in components with full type safety
55
function UserProfile({ userId }: { userId: string }) {
56
const options = userQueryOptions(userId)
57
const { data: user } = useQuery(options)
58
59
return <div>{user?.name}</div> // user is properly typed as User | undefined
60
}
61
62
// With defined initial data
63
const userWithInitialDataOptions = (userId: string, initialUser: User) =>
64
queryOptions({
65
queryKey: ['users', userId],
66
queryFn: () => fetchUser(userId),
67
initialData: initialUser, // Ensures data is always defined
68
})
69
70
function UserProfileWithInitialData({ userId, initialUser }: {
71
userId: string
72
initialUser: User
73
}) {
74
const { data: user } = useQuery(
75
userWithInitialDataOptions(userId, initialUser)
76
)
77
78
return <div>{user.name}</div> // user is typed as User (never undefined)
79
}
80
81
// Shared options across multiple components
82
export const postsQueryOptions = {
83
all: () =>
84
queryOptions({
85
queryKey: ['posts'],
86
queryFn: fetchAllPosts,
87
}),
88
89
byCategory: (category: string) =>
90
queryOptions({
91
queryKey: ['posts', { category }],
92
queryFn: () => fetchPostsByCategory(category),
93
}),
94
95
byId: (postId: string) =>
96
queryOptions({
97
queryKey: ['posts', postId],
98
queryFn: () => fetchPost(postId),
99
staleTime: Infinity, // Posts don't change often
100
}),
101
}
102
```
103
104
### infiniteQueryOptions
105
106
Creates type-safe infinite query options with enhanced TypeScript data tagging.
107
108
```typescript { .api }
109
function infiniteQueryOptions<
110
TQueryFnData = unknown,
111
TError = DefaultError,
112
TData = TQueryFnData,
113
TQueryKey extends QueryKey = QueryKey,
114
TPageParam = unknown
115
>(
116
options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
117
): UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
118
queryKey: TQueryKey
119
}
120
```
121
122
**Parameters:**
123
- `options`: `UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>` - Infinite query configuration
124
125
**Returns:** Enhanced infinite query options with improved TypeScript inference
126
127
**Key Features:**
128
- Type-safe pagination parameter handling
129
- Enhanced inference for page data structure
130
- Support for both forward and backward pagination
131
- Type tagging for better IDE support
132
133
**Example:**
134
```typescript
135
import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query'
136
137
// Reusable infinite query options
138
const postsInfiniteOptions = (category?: string) =>
139
infiniteQueryOptions({
140
queryKey: ['posts', 'infinite', { category }],
141
queryFn: ({ pageParam = 0 }) =>
142
fetchPosts({
143
page: pageParam,
144
category,
145
limit: 10
146
}),
147
getNextPageParam: (lastPage, allPages) => {
148
return lastPage.hasMore ? allPages.length : undefined
149
},
150
getPreviousPageParam: (firstPage, allPages) => {
151
return allPages.length > 1 ? 0 : undefined
152
},
153
initialPageParam: 0,
154
})
155
156
function InfinitePostsList({ category }: { category?: string }) {
157
const {
158
data,
159
fetchNextPage,
160
hasNextPage,
161
isFetchingNextPage,
162
} = useInfiniteQuery(postsInfiniteOptions(category))
163
164
return (
165
<div>
166
{data?.pages.map((page, i) => (
167
<div key={i}>
168
{page.posts.map((post) => (
169
<PostCard key={post.id} post={post} />
170
))}
171
</div>
172
))}
173
174
{hasNextPage && (
175
<button
176
onClick={() => fetchNextPage()}
177
disabled={isFetchingNextPage}
178
>
179
{isFetchingNextPage ? 'Loading...' : 'Load More'}
180
</button>
181
)}
182
</div>
183
)
184
}
185
186
// Cursor-based pagination
187
const cursorBasedPostsOptions = () =>
188
infiniteQueryOptions({
189
queryKey: ['posts', 'cursor-based'],
190
queryFn: ({ pageParam }) =>
191
fetchPostsWithCursor({ cursor: pageParam }),
192
getNextPageParam: (lastPage) => lastPage.nextCursor,
193
initialPageParam: undefined as string | undefined,
194
})
195
196
// Bidirectional infinite queries
197
const bidirectionalMessagesOptions = (channelId: string) =>
198
infiniteQueryOptions({
199
queryKey: ['messages', channelId],
200
queryFn: ({ pageParam = { direction: 'newer', cursor: null } }) =>
201
fetchMessages(channelId, pageParam),
202
getNextPageParam: (lastPage) => ({
203
direction: 'newer' as const,
204
cursor: lastPage.newestCursor,
205
}),
206
getPreviousPageParam: (firstPage) => ({
207
direction: 'older' as const,
208
cursor: firstPage.oldestCursor,
209
}),
210
initialPageParam: { direction: 'newer' as const, cursor: null },
211
})
212
```
213
214
## Mutation Configuration
215
216
### mutationOptions
217
218
Creates type-safe mutation options for consistent mutation configuration.
219
220
```typescript { .api }
221
function mutationOptions<
222
TData = unknown,
223
TError = DefaultError,
224
TVariables = void,
225
TContext = unknown
226
>(
227
options: UseMutationOptions<TData, TError, TVariables, TContext>
228
): UseMutationOptions<TData, TError, TVariables, TContext>
229
```
230
231
**Parameters:**
232
- `options`: `UseMutationOptions<TData, TError, TVariables, TContext>` - Mutation configuration object
233
234
**Returns:** Same mutation options object with enhanced type safety
235
236
**Key Features:**
237
- Type-safe mutation variable handling
238
- Enhanced context typing for optimistic updates
239
- Support for both keyed and unkeyed mutations
240
- Consistent error handling patterns
241
242
**Example:**
243
```typescript
244
import { mutationOptions, useMutation, useQueryClient } from '@tanstack/react-query'
245
246
// Define reusable mutation options
247
const createUserMutationOptions = () =>
248
mutationOptions({
249
mutationFn: async (userData: CreateUserRequest) => {
250
const response = await fetch('/api/users', {
251
method: 'POST',
252
headers: { 'Content-Type': 'application/json' },
253
body: JSON.stringify(userData),
254
})
255
return response.json() as Promise<User>
256
},
257
onSuccess: (newUser, variables, context) => {
258
// newUser is typed as User
259
// variables is typed as CreateUserRequest
260
console.log('User created:', newUser.name)
261
},
262
onError: (error, variables, context) => {
263
// Error handling with proper typing
264
console.error('Failed to create user:', error.message)
265
},
266
})
267
268
function CreateUserForm() {
269
const createUserMutation = useMutation(createUserMutationOptions())
270
271
const handleSubmit = (userData: CreateUserRequest) => {
272
createUserMutation.mutate(userData)
273
}
274
275
return (
276
<form onSubmit={(e) => {
277
e.preventDefault()
278
const formData = new FormData(e.currentTarget)
279
handleSubmit({
280
name: formData.get('name') as string,
281
email: formData.get('email') as string,
282
})
283
}}>
284
<input name="name" placeholder="Name" required />
285
<input name="email" type="email" placeholder="Email" required />
286
<button
287
type="submit"
288
disabled={createUserMutation.isPending}
289
>
290
{createUserMutation.isPending ? 'Creating...' : 'Create User'}
291
</button>
292
</form>
293
)
294
}
295
296
// Mutation with optimistic updates
297
const updateUserMutationOptions = () => {
298
const queryClient = useQueryClient()
299
300
return mutationOptions({
301
mutationKey: ['updateUser'],
302
mutationFn: async ({ userId, updates }: {
303
userId: string
304
updates: Partial<User>
305
}) => {
306
const response = await fetch(`/api/users/${userId}`, {
307
method: 'PATCH',
308
headers: { 'Content-Type': 'application/json' },
309
body: JSON.stringify(updates),
310
})
311
return response.json() as Promise<User>
312
},
313
314
// Optimistic update
315
onMutate: async ({ userId, updates }) => {
316
await queryClient.cancelQueries({ queryKey: ['users', userId] })
317
318
const previousUser = queryClient.getQueryData(['users', userId])
319
320
queryClient.setQueryData(['users', userId], (old: User) => ({
321
...old,
322
...updates,
323
}))
324
325
return { previousUser }
326
},
327
328
// Rollback on error
329
onError: (error, variables, context) => {
330
if (context?.previousUser) {
331
queryClient.setQueryData(
332
['users', variables.userId],
333
context.previousUser
334
)
335
}
336
},
337
338
// Refetch on success or error
339
onSettled: (data, error, variables) => {
340
queryClient.invalidateQueries({
341
queryKey: ['users', variables.userId]
342
})
343
},
344
})
345
}
346
347
// Keyed mutations for tracking multiple instances
348
const deletePostMutationOptions = (postId: string) =>
349
mutationOptions({
350
mutationKey: ['deletePost', postId],
351
mutationFn: async () => {
352
await fetch(`/api/posts/${postId}`, { method: 'DELETE' })
353
},
354
onSuccess: () => {
355
// Invalidate and refetch posts list
356
queryClient.invalidateQueries({ queryKey: ['posts'] })
357
},
358
})
359
360
// Batch mutation options
361
const batchUpdateMutationOptions = () =>
362
mutationOptions({
363
mutationKey: ['batchUpdate'],
364
mutationFn: async (updates: Array<{ id: string; data: Partial<User> }>) => {
365
const promises = updates.map(({ id, data }) =>
366
fetch(`/api/users/${id}`, {
367
method: 'PATCH',
368
headers: { 'Content-Type': 'application/json' },
369
body: JSON.stringify(data),
370
}).then(res => res.json())
371
)
372
return Promise.all(promises) as Promise<User[]>
373
},
374
onSuccess: (updatedUsers) => {
375
// Update cache for each user
376
updatedUsers.forEach(user => {
377
queryClient.setQueryData(['users', user.id], user)
378
})
379
},
380
})
381
```
382
383
## Advanced Configuration Patterns
384
385
### Factory Functions
386
387
Create configuration factories for common patterns:
388
389
```typescript
390
import { queryOptions, infiniteQueryOptions, mutationOptions } from '@tanstack/react-query'
391
392
// Generic CRUD query factory
393
function createCrudOptions<T>(resource: string) {
394
return {
395
list: () =>
396
queryOptions({
397
queryKey: [resource],
398
queryFn: () => fetchResource<T[]>(`/api/${resource}`),
399
}),
400
401
byId: (id: string) =>
402
queryOptions({
403
queryKey: [resource, id],
404
queryFn: () => fetchResource<T>(`/api/${resource}/${id}`),
405
}),
406
407
infinite: (filters?: Record<string, any>) =>
408
infiniteQueryOptions({
409
queryKey: [resource, 'infinite', filters],
410
queryFn: ({ pageParam = 0 }) =>
411
fetchResource<PaginatedResponse<T>>(`/api/${resource}`, {
412
page: pageParam,
413
...filters,
414
}),
415
getNextPageParam: (lastPage) => lastPage.nextPage,
416
initialPageParam: 0,
417
}),
418
419
create: () =>
420
mutationOptions({
421
mutationFn: (data: Omit<T, 'id'>) =>
422
postResource<T>(`/api/${resource}`, data),
423
}),
424
425
update: () =>
426
mutationOptions({
427
mutationFn: ({ id, data }: { id: string; data: Partial<T> }) =>
428
patchResource<T>(`/api/${resource}/${id}`, data),
429
}),
430
431
delete: () =>
432
mutationOptions({
433
mutationFn: (id: string) =>
434
deleteResource(`/api/${resource}/${id}`),
435
}),
436
}
437
}
438
439
// Usage
440
const userOptions = createCrudOptions<User>('users')
441
const postOptions = createCrudOptions<Post>('posts')
442
443
// In components
444
const { data: users } = useQuery(userOptions.list())
445
const { data: user } = useQuery(userOptions.byId('123'))
446
const createUser = useMutation(userOptions.create())
447
```
448
449
### Configuration with Default Options
450
451
Set up default configurations for your application:
452
453
```typescript
454
// Define default options
455
const defaultQueryOptions = {
456
staleTime: 5 * 60 * 1000, // 5 minutes
457
gcTime: 10 * 60 * 1000, // 10 minutes
458
retry: 3,
459
retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000),
460
}
461
462
const defaultMutationOptions = {
463
retry: 1,
464
retryDelay: 1000,
465
}
466
467
// Enhanced configuration helpers
468
const enhancedQueryOptions = <T extends UseQueryOptions>(options: T) =>
469
queryOptions({
470
...defaultQueryOptions,
471
...options,
472
})
473
474
const enhancedMutationOptions = <T extends UseMutationOptions>(options: T) =>
475
mutationOptions({
476
...defaultMutationOptions,
477
...options,
478
})
479
480
// Usage with defaults
481
const userQuery = enhancedQueryOptions({
482
queryKey: ['users', '123'],
483
queryFn: () => fetchUser('123'),
484
// Inherits all default options
485
})
486
```
487
488
## Type Definitions
489
490
```typescript { .api }
491
interface UseQueryOptions<
492
TQueryFnData = unknown,
493
TError = DefaultError,
494
TData = TQueryFnData,
495
TQueryKey extends QueryKey = QueryKey
496
> {
497
queryKey: TQueryKey
498
queryFn?: QueryFunction<TQueryFnData, TQueryKey>
499
enabled?: boolean
500
staleTime?: number
501
gcTime?: number
502
retry?: RetryValue<TError>
503
retryDelay?: RetryDelayValue<TError>
504
refetchOnMount?: boolean | 'always'
505
refetchOnWindowFocus?: boolean | 'always'
506
refetchOnReconnect?: boolean | 'always'
507
refetchInterval?: number | false
508
refetchIntervalInBackground?: boolean
509
initialData?: TData | InitialDataFunction<TData>
510
placeholderData?: TData | PlaceholderDataFunction<TData>
511
select?: (data: TQueryFnData) => TData
512
throwOnError?: ThrowOnError<TQueryFnData, TError, TData, TQueryKey>
513
// ... additional options
514
}
515
516
interface UseInfiniteQueryOptions<
517
TQueryFnData = unknown,
518
TError = DefaultError,
519
TData = TQueryFnData,
520
TQueryKey extends QueryKey = QueryKey,
521
TPageParam = unknown
522
> extends UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> {
523
getNextPageParam: GetNextPageParamFunction<TQueryFnData, TPageParam>
524
getPreviousPageParam?: GetPreviousPageParamFunction<TQueryFnData, TPageParam>
525
initialPageParam: TPageParam
526
maxPages?: number
527
}
528
529
interface UseMutationOptions<
530
TData = unknown,
531
TError = DefaultError,
532
TVariables = void,
533
TContext = unknown
534
> {
535
mutationKey?: MutationKey
536
mutationFn?: MutationFunction<TData, TVariables>
537
onMutate?: (variables: TVariables) => Promise<TContext | void> | TContext | void
538
onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown
539
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => unknown
540
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => unknown
541
retry?: RetryValue<TError>
542
retryDelay?: RetryDelayValue<TError>
543
throwOnError?: ThrowOnError<TData, TError, TVariables, unknown>
544
// ... additional options
545
}
546
```
547
548
## Best Practices
549
550
1. **Use configuration helpers** for better type safety and code reuse
551
2. **Create factory functions** for common resource patterns
552
3. **Set up default options** at the application level
553
4. **Type your data models** explicitly for better inference
554
5. **Use keyed mutations** when tracking multiple instances
555
6. **Implement optimistic updates** for better user experience
556
7. **Handle errors consistently** across your application
557
558
These configuration helpers provide the foundation for building type-safe, maintainable query and mutation logic in your React Query applications.