or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

context.mderror-handling.mdindex.mdinfinite-queries.mdmutations.mdparallel-queries.mdqueries.mdssr.mdstatus.md

infinite-queries.mddocs/

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

```