0
# Infinite Queries
1
2
Specialized query hook for paginated data with infinite scrolling support, automatic page management, and cursor-based pagination. The `useInfiniteQuery` hook is perfect for implementing "Load More" buttons or infinite scroll functionality.
3
4
## Capabilities
5
6
### useInfiniteQuery Hook
7
8
The main hook for fetching paginated data with automatic pagination management.
9
10
```typescript { .api }
11
/**
12
* Fetch paginated data with infinite loading support
13
* @param options - Infinite query configuration options
14
* @returns Infinite query result with pagination utilities
15
*/
16
function useInfiniteQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
17
options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>
18
): UseInfiniteQueryResult<TData, TError>;
19
20
/**
21
* Fetch paginated data using separate queryKey and queryFn parameters
22
* @param queryKey - Unique identifier for the query
23
* @param queryFn - Function that returns paginated data
24
* @param options - Additional infinite query configuration
25
* @returns Infinite query result with pagination utilities
26
*/
27
function useInfiniteQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
28
queryKey: TQueryKey,
29
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
30
options?: Omit<UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, 'queryKey' | 'queryFn'>
31
): UseInfiniteQueryResult<TData, TError>;
32
```
33
34
**Usage Examples:**
35
36
```typescript
37
import { useInfiniteQuery } from "react-query";
38
39
// Basic infinite query with cursor-based pagination
40
const {
41
data,
42
fetchNextPage,
43
hasNextPage,
44
isFetchingNextPage,
45
isLoading
46
} = useInfiniteQuery({
47
queryKey: ['posts'],
48
queryFn: ({ pageParam = 0 }) =>
49
fetch(`/api/posts?cursor=${pageParam}&limit=10`)
50
.then(res => res.json()),
51
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
52
getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined
53
});
54
55
// Infinite query with page-based pagination
56
const {
57
data: searchResults,
58
fetchNextPage,
59
hasNextPage,
60
isFetchingNextPage
61
} = useInfiniteQuery({
62
queryKey: ['search', searchTerm],
63
queryFn: ({ pageParam = 1 }) =>
64
fetch(`/api/search?q=${searchTerm}&page=${pageParam}&limit=20`)
65
.then(res => res.json()),
66
getNextPageParam: (lastPage, allPages) => {
67
return lastPage.hasMore ? allPages.length + 1 : undefined;
68
},
69
enabled: !!searchTerm
70
});
71
72
// Using the paginated data
73
const allPosts = data?.pages.flatMap(page => page.posts) ?? [];
74
75
return (
76
<div>
77
{allPosts.map(post => (
78
<div key={post.id}>{post.title}</div>
79
))}
80
81
{hasNextPage && (
82
<button
83
onClick={() => fetchNextPage()}
84
disabled={isFetchingNextPage}
85
>
86
{isFetchingNextPage ? 'Loading more...' : 'Load More'}
87
</button>
88
)}
89
</div>
90
);
91
```
92
93
### Infinite Query Result Interface
94
95
The return value from `useInfiniteQuery` containing paginated data and navigation utilities.
96
97
```typescript { .api }
98
interface UseInfiniteQueryResult<TData = unknown, TError = unknown> {
99
/** Paginated data structure with pages and page parameters */
100
data: InfiniteData<TData> | undefined;
101
/** Error object if the query failed */
102
error: TError | null;
103
/** Function to fetch the next page */
104
fetchNextPage: (options?: FetchNextPageOptions) => Promise<UseInfiniteQueryResult<TData, TError>>;
105
/** Function to fetch the previous page */
106
fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise<UseInfiniteQueryResult<TData, TError>>;
107
/** True if there are more pages to fetch forward */
108
hasNextPage: boolean;
109
/** True if there are more pages to fetch backward */
110
hasPreviousPage: boolean;
111
/** True if currently fetching next page */
112
isFetchingNextPage: boolean;
113
/** True if currently fetching previous page */
114
isFetchingPreviousPage: boolean;
115
/** True if this is the first time the query is loading */
116
isLoading: boolean;
117
/** True if the query is pending (loading or paused) */
118
isPending: boolean;
119
/** True if the query succeeded and has data */
120
isSuccess: boolean;
121
/** True if the query is in an error state */
122
isError: boolean;
123
/** True if the query is currently fetching any page */
124
isFetching: boolean;
125
/** True if the query is refetching in the background */
126
isRefetching: boolean;
127
/** Current status of the query */
128
status: 'pending' | 'error' | 'success';
129
/** Current fetch status */
130
fetchStatus: 'fetching' | 'paused' | 'idle';
131
/** Function to manually refetch all pages */
132
refetch: () => Promise<UseInfiniteQueryResult<TData, TError>>;
133
/** Function to remove the query from cache */
134
remove: () => void;
135
/** True if query data is stale */
136
isStale: boolean;
137
/** True if data exists in cache */
138
isPlaceholderData: boolean;
139
/** True if using previous data during refetch */
140
isPreviousData: boolean;
141
/** Timestamp when data was last updated */
142
dataUpdatedAt: number;
143
/** Timestamp when error occurred */
144
errorUpdatedAt: number;
145
/** Number of times query has failed */
146
failureCount: number;
147
/** Reason query is paused */
148
failureReason: TError | null;
149
/** True if currently paused */
150
isPaused: boolean;
151
}
152
153
interface InfiniteData<TData> {
154
/** Array of page data */
155
pages: TData[];
156
/** Array of page parameters used to fetch each page */
157
pageParams: unknown[];
158
}
159
160
interface FetchNextPageOptions {
161
/** Cancel ongoing requests before fetching */
162
cancelRefetch?: boolean;
163
}
164
165
interface FetchPreviousPageOptions {
166
/** Cancel ongoing requests before fetching */
167
cancelRefetch?: boolean;
168
}
169
```
170
171
### Infinite Query Options Interface
172
173
Configuration options for `useInfiniteQuery` hook.
174
175
```typescript { .api }
176
interface UseInfiniteQueryOptions<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey> {
177
/** Unique identifier for the query */
178
queryKey?: TQueryKey;
179
/** Function that returns paginated data */
180
queryFn?: QueryFunction<TQueryFnData, TQueryKey>;
181
/** Function to get the next page parameter */
182
getNextPageParam: GetNextPageParamFunction<TQueryFnData>;
183
/** Function to get the previous page parameter */
184
getPreviousPageParam?: GetPreviousPageParamFunction<TQueryFnData>;
185
/** Whether the query should run automatically */
186
enabled?: boolean;
187
/** Retry configuration */
188
retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
189
/** Delay between retries */
190
retryDelay?: number | ((retryAttempt: number, error: TError) => number);
191
/** Time before data is considered stale */
192
staleTime?: number;
193
/** Time before inactive queries are garbage collected */
194
cacheTime?: number;
195
/** Interval for automatic refetching */
196
refetchInterval?: number | false;
197
/** Whether to refetch when window is not focused */
198
refetchIntervalInBackground?: boolean;
199
/** When to refetch on component mount */
200
refetchOnMount?: boolean | "always";
201
/** When to refetch on window focus */
202
refetchOnWindowFocus?: boolean | "always";
203
/** When to refetch on network reconnection */
204
refetchOnReconnect?: boolean | "always";
205
/** Transform or select data subset */
206
select?: (data: InfiniteData<TQueryFnData>) => TData;
207
/** Initial data to use before first fetch */
208
initialData?: InfiniteData<TQueryFnData> | (() => InfiniteData<TQueryFnData>);
209
/** Placeholder data while loading */
210
placeholderData?: InfiniteData<TQueryFnData> | (() => InfiniteData<TQueryFnData>);
211
/** Callback on successful query */
212
onSuccess?: (data: InfiniteData<TQueryFnData>) => void;
213
/** Callback on query error */
214
onError?: (error: TError) => void;
215
/** Callback after query settles (success or error) */
216
onSettled?: (data: InfiniteData<TQueryFnData> | undefined, error: TError | null) => void;
217
/** React context for QueryClient */
218
context?: React.Context<QueryClient | undefined>;
219
/** Whether to throw errors to error boundaries */
220
useErrorBoundary?: boolean | ((error: TError) => boolean);
221
/** Whether to suspend component rendering */
222
suspense?: boolean;
223
/** Whether to keep previous data during refetch */
224
keepPreviousData?: boolean;
225
/** Additional metadata */
226
meta?: QueryMeta;
227
/** Maximum number of pages to keep in memory */
228
maxPages?: number;
229
}
230
```
231
232
### Page Parameter Functions
233
234
Functions that determine pagination parameters.
235
236
```typescript { .api }
237
type GetNextPageParamFunction<TQueryFnData = unknown> = (
238
lastPage: TQueryFnData,
239
allPages: TQueryFnData[]
240
) => unknown;
241
242
type GetPreviousPageParamFunction<TQueryFnData = unknown> = (
243
firstPage: TQueryFnData,
244
allPages: TQueryFnData[]
245
) => unknown;
246
```
247
248
## Advanced Usage Patterns
249
250
### Infinite Scroll Implementation
251
252
Automatic loading when user scrolls near the bottom:
253
254
```typescript
255
import { useInfiniteQuery } from "react-query";
256
import { useIntersectionObserver } from "./hooks/useIntersectionObserver";
257
258
function InfinitePostList() {
259
const {
260
data,
261
fetchNextPage,
262
hasNextPage,
263
isFetchingNextPage,
264
isLoading
265
} = useInfiniteQuery({
266
queryKey: ['posts'],
267
queryFn: ({ pageParam = 0 }) =>
268
fetch(`/api/posts?cursor=${pageParam}&limit=10`)
269
.then(res => res.json()),
270
getNextPageParam: (lastPage) => lastPage.nextCursor
271
});
272
273
// Intersection observer to trigger loading
274
const { ref, entry } = useIntersectionObserver({
275
threshold: 0.1,
276
rootMargin: '100px'
277
});
278
279
// Fetch next page when sentinel comes into view
280
React.useEffect(() => {
281
if (entry?.isIntersecting && hasNextPage && !isFetchingNextPage) {
282
fetchNextPage();
283
}
284
}, [entry, fetchNextPage, hasNextPage, isFetchingNextPage]);
285
286
if (isLoading) return <div>Loading...</div>;
287
288
const allPosts = data?.pages.flatMap(page => page.posts) ?? [];
289
290
return (
291
<div>
292
{allPosts.map(post => (
293
<PostCard key={post.id} post={post} />
294
))}
295
296
{/* Sentinel element for intersection observer */}
297
<div ref={ref} style={{ height: '20px' }}>
298
{isFetchingNextPage && <div>Loading more posts...</div>}
299
</div>
300
301
{!hasNextPage && (
302
<div>No more posts to load</div>
303
)}
304
</div>
305
);
306
}
307
```
308
309
### Bidirectional Infinite Loading
310
311
Supporting both forward and backward pagination:
312
313
```typescript
314
function ChatMessages({ channelId }: { channelId: string }) {
315
const {
316
data,
317
fetchNextPage,
318
fetchPreviousPage,
319
hasNextPage,
320
hasPreviousPage,
321
isFetchingNextPage,
322
isFetchingPreviousPage
323
} = useInfiniteQuery({
324
queryKey: ['messages', channelId],
325
queryFn: ({ pageParam }) => {
326
const { cursor, direction = 'newer' } = pageParam || {};
327
return fetch(`/api/channels/${channelId}/messages?cursor=${cursor}&direction=${direction}&limit=50`)
328
.then(res => res.json());
329
},
330
getNextPageParam: (lastPage) =>
331
lastPage.hasNewer ? { cursor: lastPage.newestCursor, direction: 'newer' } : undefined,
332
getPreviousPageParam: (firstPage) =>
333
firstPage.hasOlder ? { cursor: firstPage.oldestCursor, direction: 'older' } : undefined,
334
select: (data) => ({
335
pages: [...data.pages].reverse(), // Show oldest messages first
336
pageParams: data.pageParams
337
})
338
});
339
340
const allMessages = data?.pages.flatMap(page => page.messages) ?? [];
341
342
return (
343
<div className="chat-container">
344
{hasPreviousPage && (
345
<button
346
onClick={() => fetchPreviousPage()}
347
disabled={isFetchingPreviousPage}
348
>
349
{isFetchingPreviousPage ? 'Loading older...' : 'Load Older Messages'}
350
</button>
351
)}
352
353
{allMessages.map(message => (
354
<MessageComponent key={message.id} message={message} />
355
))}
356
357
{hasNextPage && (
358
<button
359
onClick={() => fetchNextPage()}
360
disabled={isFetchingNextPage}
361
>
362
{isFetchingNextPage ? 'Loading newer...' : 'Load Newer Messages'}
363
</button>
364
)}
365
</div>
366
);
367
}
368
```
369
370
### Search with Infinite Results
371
372
Combining search functionality with infinite loading:
373
374
```typescript
375
function InfiniteSearch() {
376
const [searchTerm, setSearchTerm] = useState('');
377
const [debouncedTerm, setDebouncedTerm] = useState('');
378
379
// Debounce search term
380
useEffect(() => {
381
const timer = setTimeout(() => setDebouncedTerm(searchTerm), 300);
382
return () => clearTimeout(timer);
383
}, [searchTerm]);
384
385
const {
386
data,
387
fetchNextPage,
388
hasNextPage,
389
isFetchingNextPage,
390
isLoading,
391
refetch
392
} = useInfiniteQuery({
393
queryKey: ['search', debouncedTerm],
394
queryFn: ({ pageParam = 1 }) =>
395
fetch(`/api/search?q=${debouncedTerm}&page=${pageParam}&limit=20`)
396
.then(res => res.json()),
397
getNextPageParam: (lastPage, allPages) =>
398
lastPage.hasMore ? allPages.length + 1 : undefined,
399
enabled: debouncedTerm.length > 2,
400
keepPreviousData: true // Keep showing old results while new search loads
401
});
402
403
const allResults = data?.pages.flatMap(page => page.results) ?? [];
404
405
return (
406
<div>
407
<input
408
type="text"
409
value={searchTerm}
410
onChange={(e) => setSearchTerm(e.target.value)}
411
placeholder="Search..."
412
/>
413
414
{isLoading && <div>Searching...</div>}
415
416
{allResults.length > 0 && (
417
<div>
418
<p>{data.pages[0].totalCount} results found</p>
419
{allResults.map(result => (
420
<SearchResult key={result.id} result={result} />
421
))}
422
423
{hasNextPage && (
424
<button
425
onClick={() => fetchNextPage()}
426
disabled={isFetchingNextPage}
427
>
428
{isFetchingNextPage ? 'Loading more...' : 'Load More Results'}
429
</button>
430
)}
431
</div>
432
)}
433
434
{debouncedTerm.length > 2 && allResults.length === 0 && !isLoading && (
435
<div>No results found for "{debouncedTerm}"</div>
436
)}
437
</div>
438
);
439
}
440
```
441
442
### Performance Optimization
443
444
Limiting pages in memory to prevent memory leaks:
445
446
```typescript
447
const { data } = useInfiniteQuery({
448
queryKey: ['posts'],
449
queryFn: fetchPosts,
450
getNextPageParam: (lastPage) => lastPage.nextCursor,
451
maxPages: 10, // Only keep 10 pages in memory
452
select: (data) => ({
453
pages: data.pages,
454
pageParams: data.pageParams
455
})
456
});
457
458
// Or manually clean up old pages
459
const { data, fetchNextPage } = useInfiniteQuery({
460
queryKey: ['posts'],
461
queryFn: fetchPosts,
462
getNextPageParam: (lastPage) => lastPage.nextCursor,
463
onSuccess: (data) => {
464
// Keep only last 5 pages
465
if (data.pages.length > 5) {
466
const newData = {
467
pages: data.pages.slice(-5),
468
pageParams: data.pageParams.slice(-5)
469
};
470
queryClient.setQueryData(['posts'], newData);
471
}
472
}
473
});
474
```