0
# Infinite Queries
1
2
Infinite query functionality for pagination and infinite scrolling with automatic data accumulation and page management using Angular signals.
3
4
## Capabilities
5
6
### Inject Infinite Query
7
8
Creates an infinite query that can load data page by page, perfect for pagination and infinite scrolling scenarios.
9
10
```typescript { .api }
11
/**
12
* Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
13
* Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll"
14
* @param injectInfiniteQueryFn - A function that returns infinite query options
15
* @param options - Additional configuration including custom injector
16
* @returns The infinite query result with signals and page management functions
17
*/
18
function injectInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
19
injectInfiniteQueryFn: () => CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
20
options?: InjectInfiniteQueryOptions
21
): CreateInfiniteQueryResult<TData, TError>;
22
23
// Overloads for different initial data scenarios
24
function injectInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
25
injectInfiniteQueryFn: () => DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
26
options?: InjectInfiniteQueryOptions
27
): DefinedCreateInfiniteQueryResult<TData, TError>;
28
29
function injectInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
30
injectInfiniteQueryFn: () => UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
31
options?: InjectInfiniteQueryOptions
32
): CreateInfiniteQueryResult<TData, TError>;
33
```
34
35
**Usage Examples:**
36
37
```typescript
38
import { injectInfiniteQuery } from "@tanstack/angular-query-experimental";
39
import { Component, inject } from "@angular/core";
40
import { HttpClient } from "@angular/common/http";
41
42
@Component({
43
selector: 'app-posts-list',
44
template: `
45
<div *ngFor="let page of postsQuery.data()?.pages; let pageIndex = index">
46
<div *ngFor="let post of page.posts">
47
<h3>{{ post.title }}</h3>
48
<p>{{ post.content }}</p>
49
</div>
50
</div>
51
52
<button
53
*ngIf="postsQuery.hasNextPage()"
54
(click)="postsQuery.fetchNextPage()"
55
[disabled]="postsQuery.isFetchingNextPage()"
56
>
57
{{ postsQuery.isFetchingNextPage() ? 'Loading...' : 'Load More' }}
58
</button>
59
60
<div *ngIf="postsQuery.isError()">
61
Error: {{ postsQuery.error()?.message }}
62
</div>
63
`
64
})
65
export class PostsListComponent {
66
#http = inject(HttpClient);
67
68
// Basic infinite query for pagination
69
postsQuery = injectInfiniteQuery(() => ({
70
queryKey: ['posts'],
71
queryFn: ({ pageParam = 1 }) =>
72
this.#http.get<PostsPage>(`/api/posts?page=${pageParam}&limit=10`),
73
initialPageParam: 1,
74
getNextPageParam: (lastPage, allPages) => {
75
return lastPage.hasMore ? lastPage.nextPage : null;
76
},
77
getPreviousPageParam: (firstPage, allPages) => {
78
return firstPage.page > 1 ? firstPage.page - 1 : null;
79
}
80
}));
81
82
// Infinite query with cursor-based pagination
83
messagesQuery = injectInfiniteQuery(() => ({
84
queryKey: ['messages'],
85
queryFn: ({ pageParam = null }) => {
86
const url = pageParam
87
? `/api/messages?cursor=${pageParam}&limit=20`
88
: '/api/messages?limit=20';
89
return this.#http.get<MessagesPage>(url);
90
},
91
initialPageParam: null as string | null,
92
getNextPageParam: (lastPage) => lastPage.nextCursor,
93
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
94
staleTime: 5 * 60 * 1000 // 5 minutes
95
}));
96
}
97
98
interface PostsPage {
99
posts: Post[];
100
page: number;
101
hasMore: boolean;
102
nextPage: number;
103
}
104
105
interface MessagesPage {
106
messages: Message[];
107
nextCursor: string | null;
108
prevCursor: string | null;
109
}
110
111
interface Post {
112
id: number;
113
title: string;
114
content: string;
115
}
116
117
interface Message {
118
id: string;
119
text: string;
120
timestamp: string;
121
}
122
```
123
124
### Infinite Query Options Interface
125
126
Comprehensive options for configuring infinite query behavior.
127
128
```typescript { .api }
129
interface CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> {
130
/** Unique key for the query, used for caching and invalidation */
131
queryKey: TQueryKey;
132
/** Function that returns a promise resolving to the page data */
133
queryFn: InfiniteQueryFunction<TQueryFnData, TQueryKey, TPageParam>;
134
/** Initial page parameter for the first page */
135
initialPageParam: TPageParam;
136
/** Function to determine the next page parameter */
137
getNextPageParam: (
138
lastPage: TQueryFnData,
139
allPages: TQueryFnData[],
140
lastPageParam: TPageParam,
141
allPageParams: TPageParam[]
142
) => TPageParam | null | undefined;
143
/** Function to determine the previous page parameter */
144
getPreviousPageParam?: (
145
firstPage: TQueryFnData,
146
allPages: TQueryFnData[],
147
firstPageParam: TPageParam,
148
allPageParams: TPageParam[]
149
) => TPageParam | null | undefined;
150
/** Whether the query should automatically execute */
151
enabled?: boolean;
152
/** Time in milliseconds after which data is considered stale */
153
staleTime?: number;
154
/** Time in milliseconds after which unused data is garbage collected */
155
gcTime?: number;
156
/** Whether to refetch when window regains focus */
157
refetchOnWindowFocus?: boolean;
158
/** Whether to refetch when component reconnects */
159
refetchOnReconnect?: boolean;
160
/** Interval in milliseconds for automatic refetching */
161
refetchInterval?: number;
162
/** Whether to continue refetching while window is hidden */
163
refetchIntervalInBackground?: boolean;
164
/** Number of retry attempts on failure */
165
retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
166
/** Delay function for retry attempts */
167
retryDelay?: number | ((retryAttempt: number, error: TError) => number);
168
/** Function to transform query data */
169
select?: (data: InfiniteData<TQueryFnData, TPageParam>) => TData;
170
/** Initial data to use while loading */
171
initialData?: InfiniteData<TQueryFnData, TPageParam> | (() => InfiniteData<TQueryFnData, TPageParam>);
172
/** Placeholder data to show while loading */
173
placeholderData?: InfiniteData<TQueryFnData, TPageParam> | ((previousData: InfiniteData<TQueryFnData, TPageParam> | undefined) => InfiniteData<TQueryFnData, TPageParam>);
174
/** Maximum number of pages to store */
175
maxPages?: number;
176
/** Whether to use infinite queries structure sharing */
177
structuralSharing?: boolean | ((oldData: InfiniteData<TQueryFnData, TPageParam> | undefined, newData: InfiniteData<TQueryFnData, TPageParam>) => InfiniteData<TQueryFnData, TPageParam>);
178
}
179
```
180
181
### Infinite Query Result Interface
182
183
Signal-based result object providing reactive access to infinite query state and page management.
184
185
```typescript { .api }
186
interface CreateInfiniteQueryResult<TData, TError> {
187
/** Signal containing all pages of data */
188
data: Signal<InfiniteData<TData> | undefined>;
189
/** Signal containing any error that occurred */
190
error: Signal<TError | null>;
191
/** Signal indicating if query is currently loading (first time) */
192
isLoading: Signal<boolean>;
193
/** Signal indicating if query is pending (loading or fetching) */
194
isPending: Signal<boolean>;
195
/** Signal indicating if query completed successfully */
196
isSuccess: Signal<boolean>;
197
/** Signal indicating if query resulted in error */
198
isError: Signal<boolean>;
199
/** Signal indicating if query is currently fetching */
200
isFetching: Signal<boolean>;
201
/** Signal indicating if query is refetching in background */
202
isRefetching: Signal<boolean>;
203
/** Signal indicating if there is a next page available */
204
hasNextPage: Signal<boolean>;
205
/** Signal indicating if there is a previous page available */
206
hasPreviousPage: Signal<boolean>;
207
/** Signal indicating if currently fetching next page */
208
isFetchingNextPage: Signal<boolean>;
209
/** Signal indicating if currently fetching previous page */
210
isFetchingPreviousPage: Signal<boolean>;
211
/** Signal containing query status */
212
status: Signal<'pending' | 'error' | 'success'>;
213
/** Signal containing fetch status */
214
fetchStatus: Signal<'fetching' | 'paused' | 'idle'>;
215
/** Signal containing current failure count */
216
failureCount: Signal<number>;
217
/** Signal containing failure reason */
218
failureReason: Signal<TError | null>;
219
220
/** Function to fetch the next page */
221
fetchNextPage: (options?: { cancelRefetch?: boolean }) => Promise<InfiniteQueryObserverResult<TData, TError>>;
222
/** Function to fetch the previous page */
223
fetchPreviousPage: (options?: { cancelRefetch?: boolean }) => Promise<InfiniteQueryObserverResult<TData, TError>>;
224
225
// Type narrowing methods
226
isSuccess(this: CreateInfiniteQueryResult<TData, TError>): this is CreateInfiniteQueryResult<TData, TError>;
227
isError(this: CreateInfiniteQueryResult<TData, TError>): this is CreateInfiniteQueryResult<TData, TError>;
228
isPending(this: CreateInfiniteQueryResult<TData, TError>): this is CreateInfiniteQueryResult<TData, TError>;
229
}
230
231
interface DefinedCreateInfiniteQueryResult<TData, TError> extends CreateInfiniteQueryResult<TData, TError> {
232
/** Signal containing all pages of data (guaranteed to be defined) */
233
data: Signal<InfiniteData<TData>>;
234
}
235
```
236
237
### Infinite Data Structure
238
239
Type definition for the structure that holds infinite query data.
240
241
```typescript { .api }
242
interface InfiniteData<TData, TPageParam = unknown> {
243
/** Array of all loaded pages */
244
pages: TData[];
245
/** Array of page parameters corresponding to each page */
246
pageParams: TPageParam[];
247
}
248
```
249
250
### Query Function Type
251
252
Type definition for infinite query functions.
253
254
```typescript { .api }
255
type InfiniteQueryFunction<TQueryFnData, TQueryKey, TPageParam> = (
256
context: {
257
queryKey: TQueryKey;
258
pageParam: TPageParam;
259
direction: 'forward' | 'backward';
260
meta: Record<string, unknown> | undefined;
261
signal: AbortSignal;
262
}
263
) => Promise<TQueryFnData>;
264
```
265
266
### Options Configuration
267
268
Configuration interface for injectInfiniteQuery behavior.
269
270
```typescript { .api }
271
interface InjectInfiniteQueryOptions {
272
/**
273
* The Injector in which to create the infinite query.
274
* If not provided, the current injection context will be used instead (via inject).
275
*/
276
injector?: Injector;
277
}
278
```
279
280
### Helper Types for Initial Data
281
282
Type definitions for different initial data scenarios.
283
284
```typescript { .api }
285
type UndefinedInitialDataInfiniteOptions<
286
TQueryFnData,
287
TError,
288
TData,
289
TQueryKey,
290
TPageParam
291
> = CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
292
initialData?: undefined | NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>> | InitialDataFunction<NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>>;
293
};
294
295
type DefinedInitialDataInfiniteOptions<
296
TQueryFnData,
297
TError,
298
TData,
299
TQueryKey,
300
TPageParam
301
> = CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
302
initialData: NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>> | (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>);
303
};
304
```
305
306
## Advanced Usage Patterns
307
308
### Infinite Scrolling
309
310
```typescript
311
@Component({
312
selector: 'app-infinite-scroll',
313
template: `
314
<div
315
class="infinite-scroll-container"
316
(scroll)="onScroll($event)"
317
>
318
<div *ngFor="let page of feedQuery.data()?.pages">
319
<div *ngFor="let item of page.items" class="feed-item">
320
{{ item.title }}
321
</div>
322
</div>
323
324
<div *ngIf="feedQuery.isFetchingNextPage()" class="loading">
325
Loading more...
326
</div>
327
</div>
328
`
329
})
330
export class InfiniteScrollComponent {
331
#http = inject(HttpClient);
332
333
feedQuery = injectInfiniteQuery(() => ({
334
queryKey: ['feed'],
335
queryFn: ({ pageParam = 0 }) =>
336
this.#http.get<FeedPage>(`/api/feed?offset=${pageParam}&limit=20`),
337
initialPageParam: 0,
338
getNextPageParam: (lastPage, allPages) => {
339
return lastPage.hasMore ? lastPage.nextOffset : null;
340
}
341
}));
342
343
onScroll(event: Event) {
344
const element = event.target as HTMLElement;
345
const threshold = 200; // pixels from bottom
346
347
if (element.scrollTop + element.clientHeight >= element.scrollHeight - threshold) {
348
if (this.feedQuery.hasNextPage() && !this.feedQuery.isFetchingNextPage()) {
349
this.feedQuery.fetchNextPage();
350
}
351
}
352
}
353
}
354
```
355
356
### Bidirectional Pagination
357
358
```typescript
359
@Component({})
360
export class BidirectionalPaginationComponent {
361
#http = inject(HttpClient);
362
363
commentsQuery = injectInfiniteQuery(() => ({
364
queryKey: ['comments', 'post-123'],
365
queryFn: ({ pageParam = { cursor: null, direction: 'forward' } }) => {
366
const params = new URLSearchParams();
367
if (pageParam.cursor) {
368
params.set('cursor', pageParam.cursor);
369
}
370
params.set('direction', pageParam.direction);
371
return this.#http.get<CommentsPage>(`/api/comments?${params}`);
372
},
373
initialPageParam: { cursor: null, direction: 'forward' } as const,
374
getNextPageParam: (lastPage) => {
375
return lastPage.nextCursor
376
? { cursor: lastPage.nextCursor, direction: 'forward' as const }
377
: null;
378
},
379
getPreviousPageParam: (firstPage) => {
380
return firstPage.prevCursor
381
? { cursor: firstPage.prevCursor, direction: 'backward' as const }
382
: null;
383
}
384
}));
385
386
loadNewer() {
387
if (this.commentsQuery.hasPreviousPage()) {
388
this.commentsQuery.fetchPreviousPage();
389
}
390
}
391
392
loadOlder() {
393
if (this.commentsQuery.hasNextPage()) {
394
this.commentsQuery.fetchNextPage();
395
}
396
}
397
}
398
```
399
400
### Search with Infinite Results
401
402
```typescript
403
@Component({})
404
export class InfiniteSearchComponent {
405
#http = inject(HttpClient);
406
407
searchTerm = signal('');
408
409
searchQuery = injectInfiniteQuery(() => ({
410
queryKey: ['search', this.searchTerm()],
411
queryFn: ({ pageParam = 1 }) => {
412
const term = this.searchTerm();
413
return this.#http.get<SearchResults>(`/api/search?q=${term}&page=${pageParam}`);
414
},
415
initialPageParam: 1,
416
getNextPageParam: (lastPage, allPages) => {
417
return lastPage.hasMore ? allPages.length + 1 : null;
418
},
419
enabled: () => this.searchTerm().length > 2,
420
staleTime: 5 * 60 * 1000 // 5 minutes
421
}));
422
423
// Get flat array of all results across pages
424
get allResults() {
425
return this.searchQuery.data()?.pages.flatMap(page => page.results) ?? [];
426
}
427
}
428
```
429
430
### Managing Memory Usage
431
432
```typescript
433
@Component({})
434
export class MemoryOptimizedComponent {
435
#http = inject(HttpClient);
436
437
dataQuery = injectInfiniteQuery(() => ({
438
queryKey: ['large-dataset'],
439
queryFn: ({ pageParam = 1 }) =>
440
this.#http.get<DataPage>(`/api/data?page=${pageParam}`),
441
initialPageParam: 1,
442
getNextPageParam: (lastPage, allPages) => {
443
return lastPage.hasMore ? allPages.length + 1 : null;
444
},
445
maxPages: 10, // Only keep last 10 pages in memory
446
staleTime: 30 * 1000, // 30 seconds
447
gcTime: 5 * 60 * 1000 // 5 minutes
448
}));
449
}
450
```