or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mdinfinite-queries.mdmulti-query-operations.mdmutation-management.mdoptions-helpers.mdprovider-setup.mdquery-management.mdstatus-monitoring.md

infinite-queries.mddocs/

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

```