0
# Core Query Hooks
1
2
The foundation hooks for data fetching, caching, and synchronization in React Query.
3
4
## useQuery
5
6
**Primary hook for fetching, caching and updating asynchronous data**
7
8
```typescript { .api }
9
function useQuery<
10
TQueryFnData = unknown,
11
TError = DefaultError,
12
TData = TQueryFnData,
13
TQueryKey extends QueryKey = QueryKey,
14
>(
15
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
16
queryClient?: QueryClient,
17
): UseQueryResult<TData, TError>
18
19
// With defined initial data
20
function useQuery<...>(
21
options: DefinedInitialDataOptions<...>,
22
queryClient?: QueryClient,
23
): DefinedUseQueryResult<TData, TError>
24
25
// With undefined initial data
26
function useQuery<...>(
27
options: UndefinedInitialDataOptions<...>,
28
queryClient?: QueryClient,
29
): UseQueryResult<TData, TError>
30
```
31
32
### Options
33
34
```typescript { .api }
35
interface UseQueryOptions<
36
TQueryFnData = unknown,
37
TError = DefaultError,
38
TData = TQueryFnData,
39
TQueryKey extends QueryKey = QueryKey,
40
> {
41
queryKey: TQueryKey
42
queryFn?: QueryFunction<TQueryFnData, TQueryKey> | SkipToken
43
enabled?: boolean
44
networkMode?: 'online' | 'always' | 'offlineFirst'
45
retry?: boolean | number | ((failureCount: number, error: TError) => boolean)
46
retryDelay?: number | ((retryAttempt: number, error: TError) => number)
47
staleTime?: number | ((query: Query) => number)
48
gcTime?: number
49
queryKeyHashFn?: QueryKeyHashFunction<TQueryKey>
50
refetchInterval?: number | false | ((query: Query) => number | false)
51
refetchIntervalInBackground?: boolean
52
refetchOnMount?: boolean | 'always' | ((query: Query) => boolean | 'always')
53
refetchOnWindowFocus?: boolean | 'always' | ((query: Query) => boolean | 'always')
54
refetchOnReconnect?: boolean | 'always' | ((query: Query) => boolean | 'always')
55
notifyOnChangeProps?: Array<keyof UseQueryResult> | 'all'
56
onSuccess?: (data: TData) => void
57
onError?: (error: TError) => void
58
onSettled?: (data: TData | undefined, error: TError | null) => void
59
select?: (data: TQueryFnData) => TData
60
suspense?: boolean
61
initialData?: TData | InitialDataFunction<TData>
62
initialDataUpdatedAt?: number | (() => number)
63
placeholderData?: TData | PlaceholderDataFunction<TData, TError>
64
structuralSharing?: boolean | ((oldData: TData | undefined, newData: TData) => TData)
65
throwOnError?: boolean | ((error: TError, query: Query) => boolean)
66
meta?: Record<string, unknown>
67
}
68
```
69
70
### Result
71
72
```typescript { .api }
73
interface UseQueryResult<TData = unknown, TError = DefaultError> {
74
data: TData | undefined
75
dataUpdatedAt: number
76
error: TError | null
77
errorUpdatedAt: number
78
failureCount: number
79
failureReason: TError | null
80
fetchStatus: 'fetching' | 'paused' | 'idle'
81
isError: boolean
82
isFetched: boolean
83
isFetchedAfterMount: boolean
84
isFetching: boolean
85
isInitialLoading: boolean
86
isLoading: boolean
87
isLoadingError: boolean
88
isPaused: boolean
89
isPending: boolean
90
isPlaceholderData: boolean
91
isRefetchError: boolean
92
isRefetching: boolean
93
isStale: boolean
94
isSuccess: boolean
95
refetch: (options?: RefetchOptions) => Promise<UseQueryResult<TData, TError>>
96
status: 'pending' | 'error' | 'success'
97
}
98
99
interface DefinedUseQueryResult<TData = unknown, TError = DefaultError>
100
extends Omit<UseQueryResult<TData, TError>, 'data'> {
101
data: TData
102
}
103
```
104
105
### Basic Usage
106
107
```typescript { .api }
108
import { useQuery } from '@tanstack/react-query'
109
110
interface User {
111
id: number
112
name: string
113
email: string
114
}
115
116
function UserProfile({ userId }: { userId: number }) {
117
const {
118
data: user,
119
isLoading,
120
isError,
121
error,
122
refetch
123
} = useQuery<User>({
124
queryKey: ['user', userId],
125
queryFn: async () => {
126
const response = await fetch(`/api/users/${userId}`)
127
if (!response.ok) {
128
throw new Error('Failed to fetch user')
129
}
130
return response.json()
131
},
132
staleTime: 5 * 60 * 1000, // 5 minutes
133
gcTime: 10 * 60 * 1000, // 10 minutes
134
})
135
136
if (isLoading) return <div>Loading user...</div>
137
if (isError) return <div>Error: {error.message}</div>
138
139
return (
140
<div>
141
<h1>{user?.name}</h1>
142
<p>{user?.email}</p>
143
<button onClick={() => refetch()}>Refresh</button>
144
</div>
145
)
146
}
147
```
148
149
### Advanced Usage
150
151
```typescript { .api }
152
// Conditional querying
153
function UserPosts({ userId }: { userId?: number }) {
154
const { data, isLoading } = useQuery({
155
queryKey: ['user-posts', userId],
156
queryFn: () => fetchUserPosts(userId!),
157
enabled: !!userId, // Only fetch when userId exists
158
retry: (failureCount, error) => {
159
// Don't retry on 404
160
if (error.status === 404) return false
161
return failureCount < 3
162
}
163
})
164
165
// Component logic...
166
}
167
168
// Data transformation
169
function PostsList() {
170
const { data: posts } = useQuery({
171
queryKey: ['posts'],
172
queryFn: fetchPosts,
173
select: (data) => data.posts.filter(post => post.published),
174
placeholderData: { posts: [] }
175
})
176
177
return (
178
<div>
179
{posts.map(post => (
180
<div key={post.id}>{post.title}</div>
181
))}
182
</div>
183
)
184
}
185
```
186
187
## useInfiniteQuery
188
189
**Hook for queries that can incrementally load more data (pagination, infinite scrolling)**
190
191
```typescript { .api }
192
function useInfiniteQuery<
193
TQueryFnData,
194
TError = DefaultError,
195
TData = InfiniteData<TQueryFnData>,
196
TQueryKey extends QueryKey = QueryKey,
197
TPageParam = unknown,
198
>(
199
options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
200
queryClient?: QueryClient,
201
): UseInfiniteQueryResult<TData, TError>
202
203
// With defined initial data
204
function useInfiniteQuery<...>(
205
options: DefinedInitialDataInfiniteOptions<...>,
206
queryClient?: QueryClient,
207
): DefinedUseInfiniteQueryResult<TData, TError>
208
209
// With undefined initial data
210
function useInfiniteQuery<...>(
211
options: UndefinedInitialDataInfiniteOptions<...>,
212
queryClient?: QueryClient,
213
): UseInfiniteQueryResult<TData, TError>
214
```
215
216
### Options
217
218
```typescript { .api }
219
interface UseInfiniteQueryOptions<
220
TQueryFnData = unknown,
221
TError = DefaultError,
222
TData = TQueryFnData,
223
TQueryKey extends QueryKey = QueryKey,
224
TPageParam = unknown,
225
> extends Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryFn'> {
226
queryFn: QueryFunction<TQueryFnData, TQueryKey, TPageParam>
227
initialPageParam: TPageParam
228
getNextPageParam: (
229
lastPage: TQueryFnData,
230
allPages: TQueryFnData[],
231
lastPageParam: TPageParam,
232
allPageParams: TPageParam[]
233
) => TPageParam | undefined | null
234
getPreviousPageParam?: (
235
firstPage: TQueryFnData,
236
allPages: TQueryFnData[],
237
firstPageParam: TPageParam,
238
allPageParams: TPageParam[]
239
) => TPageParam | undefined | null
240
maxPages?: number
241
}
242
```
243
244
### Result
245
246
```typescript { .api }
247
interface UseInfiniteQueryResult<TData = unknown, TError = DefaultError>
248
extends Omit<UseQueryResult<TData, TError>, 'data'> {
249
data: InfiniteData<TData> | undefined
250
fetchNextPage: (options?: FetchNextPageOptions) => Promise<UseInfiniteQueryResult<TData, TError>>
251
fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise<UseInfiniteQueryResult<TData, TError>>
252
hasNextPage: boolean
253
hasPreviousPage: boolean
254
isFetchingNextPage: boolean
255
isFetchingPreviousPage: boolean
256
}
257
258
interface InfiniteData<TData, TPageParam = unknown> {
259
pages: TData[]
260
pageParams: TPageParam[]
261
}
262
```
263
264
### Basic Usage
265
266
```typescript { .api }
267
interface PostsPage {
268
posts: Post[]
269
nextCursor?: number
270
hasMore: boolean
271
}
272
273
function InfinitePostsList() {
274
const {
275
data,
276
fetchNextPage,
277
hasNextPage,
278
isFetchingNextPage,
279
isLoading,
280
error
281
} = useInfiniteQuery<PostsPage>({
282
queryKey: ['posts'],
283
queryFn: async ({ pageParam = 0 }) => {
284
const response = await fetch(`/api/posts?cursor=${pageParam}`)
285
return response.json()
286
},
287
initialPageParam: 0,
288
getNextPageParam: (lastPage) => {
289
return lastPage.hasMore ? lastPage.nextCursor : undefined
290
},
291
staleTime: 5 * 60 * 1000,
292
})
293
294
if (isLoading) return <div>Loading posts...</div>
295
if (error) return <div>Error: {error.message}</div>
296
297
return (
298
<div>
299
{data?.pages.map((page, i) => (
300
<div key={i}>
301
{page.posts.map((post) => (
302
<div key={post.id}>
303
<h3>{post.title}</h3>
304
<p>{post.excerpt}</p>
305
</div>
306
))}
307
</div>
308
))}
309
310
<button
311
onClick={() => fetchNextPage()}
312
disabled={!hasNextPage || isFetchingNextPage}
313
>
314
{isFetchingNextPage
315
? 'Loading more...'
316
: hasNextPage
317
? 'Load More'
318
: 'Nothing more to load'}
319
</button>
320
</div>
321
)
322
}
323
```
324
325
### Bidirectional Infinite Queries
326
327
```typescript { .api }
328
function BidirectionalFeed() {
329
const {
330
data,
331
fetchNextPage,
332
fetchPreviousPage,
333
hasNextPage,
334
hasPreviousPage,
335
isFetchingNextPage,
336
isFetchingPreviousPage
337
} = useInfiniteQuery({
338
queryKey: ['feed'],
339
queryFn: async ({ pageParam = { direction: 'next', cursor: 0 } }) => {
340
const { direction, cursor } = pageParam
341
const response = await fetch(`/api/feed?${direction}=${cursor}`)
342
return response.json()
343
},
344
initialPageParam: { direction: 'next', cursor: 0 },
345
getNextPageParam: (lastPage) =>
346
lastPage.hasMore
347
? { direction: 'next', cursor: lastPage.nextCursor }
348
: undefined,
349
getPreviousPageParam: (firstPage) =>
350
firstPage.hasPrevious
351
? { direction: 'previous', cursor: firstPage.previousCursor }
352
: undefined
353
})
354
355
return (
356
<div>
357
<button
358
onClick={() => fetchPreviousPage()}
359
disabled={!hasPreviousPage || isFetchingPreviousPage}
360
>
361
{isFetchingPreviousPage ? 'Loading...' : 'Load Previous'}
362
</button>
363
364
{data?.pages.map((page, i) => (
365
<div key={i}>
366
{page.items.map(item => (
367
<div key={item.id}>{item.content}</div>
368
))}
369
</div>
370
))}
371
372
<button
373
onClick={() => fetchNextPage()}
374
disabled={!hasNextPage || isFetchingNextPage}
375
>
376
{isFetchingNextPage ? 'Loading...' : 'Load Next'}
377
</button>
378
</div>
379
)
380
}
381
```
382
383
## useQueries
384
385
**Hook for running multiple queries in parallel with advanced composition capabilities**
386
387
```typescript { .api }
388
function useQueries<
389
T extends Array<any>,
390
TCombinedResult = QueriesResults<T>,
391
>(
392
options: {
393
queries: readonly [...QueriesOptions<T>]
394
combine?: (result: QueriesResults<T>) => TCombinedResult
395
subscribed?: boolean
396
},
397
queryClient?: QueryClient,
398
): TCombinedResult
399
```
400
401
### Types
402
403
```typescript { .api }
404
type QueriesOptions<T extends Array<any>> = {
405
[K in keyof T]: UseQueryOptions<any, any, any, any>
406
}
407
408
type QueriesResults<T extends Array<any>> = {
409
[K in keyof T]: UseQueryResult<any, any>
410
}
411
```
412
413
### Basic Usage
414
415
```typescript { .api }
416
function Dashboard({ userId }: { userId: number }) {
417
const results = useQueries({
418
queries: [
419
{
420
queryKey: ['user', userId],
421
queryFn: () => fetchUser(userId),
422
staleTime: 5 * 60 * 1000,
423
},
424
{
425
queryKey: ['user-posts', userId],
426
queryFn: () => fetchUserPosts(userId),
427
staleTime: 2 * 60 * 1000,
428
},
429
{
430
queryKey: ['user-followers', userId],
431
queryFn: () => fetchUserFollowers(userId),
432
enabled: userId > 0,
433
}
434
]
435
})
436
437
const [userQuery, postsQuery, followersQuery] = results
438
439
if (userQuery.isLoading) return <div>Loading user...</div>
440
if (userQuery.error) return <div>Error loading user</div>
441
442
return (
443
<div>
444
<h1>{userQuery.data?.name}</h1>
445
446
<section>
447
<h2>Posts</h2>
448
{postsQuery.isLoading ? (
449
<div>Loading posts...</div>
450
) : (
451
<div>{postsQuery.data?.length} posts</div>
452
)}
453
</section>
454
455
<section>
456
<h2>Followers</h2>
457
{followersQuery.isLoading ? (
458
<div>Loading followers...</div>
459
) : (
460
<div>{followersQuery.data?.length} followers</div>
461
)}
462
</section>
463
</div>
464
)
465
}
466
```
467
468
### With Combine Function
469
470
```typescript { .api }
471
function CombinedDashboard({ userIds }: { userIds: number[] }) {
472
const combinedResult = useQueries({
473
queries: userIds.map(id => ({
474
queryKey: ['user', id],
475
queryFn: () => fetchUser(id),
476
})),
477
combine: (results) => ({
478
users: results.map(result => result.data).filter(Boolean),
479
isLoading: results.some(result => result.isLoading),
480
hasErrors: results.some(result => result.isError),
481
errors: results.map(result => result.error).filter(Boolean),
482
})
483
})
484
485
if (combinedResult.isLoading) {
486
return <div>Loading users...</div>
487
}
488
489
if (combinedResult.hasErrors) {
490
return (
491
<div>
492
Errors occurred:
493
{combinedResult.errors.map((error, i) => (
494
<div key={i}>{error.message}</div>
495
))}
496
</div>
497
)
498
}
499
500
return (
501
<div>
502
<h1>Users ({combinedResult.users.length})</h1>
503
{combinedResult.users.map(user => (
504
<div key={user.id}>{user.name}</div>
505
))}
506
</div>
507
)
508
}
509
```
510
511
### Dynamic Queries
512
513
```typescript { .api }
514
function DynamicQueries({ searchTerms }: { searchTerms: string[] }) {
515
const queries = useQueries({
516
queries: searchTerms.map((term) => ({
517
queryKey: ['search', term],
518
queryFn: () => searchPosts(term),
519
enabled: term.length > 2, // Only search terms longer than 2 chars
520
staleTime: 30 * 1000, // 30 seconds
521
})),
522
combine: (results) => ({
523
data: results.flatMap(result => result.data || []),
524
isAnyLoading: results.some(result => result.isLoading),
525
hasData: results.some(result => result.data?.length > 0),
526
})
527
})
528
529
return (
530
<div>
531
{queries.isAnyLoading && <div>Searching...</div>}
532
{!queries.hasData && !queries.isAnyLoading && (
533
<div>No results found</div>
534
)}
535
{queries.data.map(post => (
536
<div key={post.id}>{post.title}</div>
537
))}
538
</div>
539
)
540
}
541
```
542
543
## Performance Optimizations
544
545
### Query Key Factories
546
547
```typescript { .api }
548
// Consistent query key management
549
const userKeys = {
550
all: ['users'] as const,
551
lists: () => [...userKeys.all, 'list'] as const,
552
list: (filters: string) => [...userKeys.lists(), { filters }] as const,
553
details: () => [...userKeys.all, 'detail'] as const,
554
detail: (id: number) => [...userKeys.details(), id] as const,
555
posts: (id: number) => [...userKeys.detail(id), 'posts'] as const,
556
}
557
558
// Usage with type safety
559
const { data: user } = useQuery({
560
queryKey: userKeys.detail(userId),
561
queryFn: () => fetchUser(userId)
562
})
563
564
const { data: posts } = useQuery({
565
queryKey: userKeys.posts(userId),
566
queryFn: () => fetchUserPosts(userId),
567
enabled: !!user
568
})
569
```
570
571
### Structural Sharing
572
573
```typescript { .api }
574
const { data } = useQuery({
575
queryKey: ['todos'],
576
queryFn: fetchTodos,
577
structuralSharing: (oldData, newData) => {
578
// Custom structural sharing logic
579
if (!oldData) return newData
580
581
// Only update if data actually changed
582
const changed = newData.some((todo, index) =>
583
!oldData[index] || todo.id !== oldData[index].id
584
)
585
586
return changed ? newData : oldData
587
}
588
})
589
```
590
591
### Selective Subscriptions
592
593
```typescript { .api }
594
const { data, refetch } = useQuery({
595
queryKey: ['user', userId],
596
queryFn: () => fetchUser(userId),
597
notifyOnChangeProps: ['data', 'error'], // Only re-render when data or error changes
598
})
599
```
600
601
These core query hooks provide the foundation for all data fetching patterns in React Query, offering powerful caching, background updates, error handling, and performance optimizations out of the box.