0
# Infinite Loading
1
2
The `useSWRInfinite` hook provides pagination and infinite loading capabilities with automatic page management, size control, and optimized revalidation.
3
4
## Capabilities
5
6
### useSWRInfinite Hook
7
8
Hook for infinite loading and pagination scenarios with automatic page management.
9
10
```typescript { .api }
11
/**
12
* Hook for infinite loading and pagination scenarios
13
* @param getKey - Function that returns the key for each page
14
* @param fetcher - Function that fetches data for each page
15
* @param config - Configuration options extending SWRConfiguration
16
* @returns SWRInfiniteResponse with data array, size control, and loading states
17
*/
18
function useSWRInfinite<Data = any, Error = any>(
19
getKey: SWRInfiniteKeyLoader<Data>,
20
fetcher?: SWRInfiniteFetcher<Data> | null,
21
config?: SWRInfiniteConfiguration<Data, Error>
22
): SWRInfiniteResponse<Data, Error>;
23
24
function unstable_serialize(getKey: SWRInfiniteKeyLoader): string | undefined;
25
```
26
27
**Usage Examples:**
28
29
```typescript
30
import useSWRInfinite from "swr/infinite";
31
32
// Basic infinite loading
33
const getKey = (pageIndex: number, previousPageData: any) => {
34
if (previousPageData && !previousPageData.length) return null; // reached the end
35
return `/api/issues?page=${pageIndex}&limit=10`;
36
};
37
38
const { data, error, size, setSize, isValidating } = useSWRInfinite(
39
getKey,
40
fetcher
41
);
42
43
// Cursor-based pagination
44
const getKey = (pageIndex: number, previousPageData: any) => {
45
if (previousPageData && !previousPageData.nextCursor) return null;
46
if (pageIndex === 0) return "/api/posts?limit=10";
47
return `/api/posts?cursor=${previousPageData.nextCursor}&limit=10`;
48
};
49
50
// Load more functionality
51
const issues = data ? data.flat() : [];
52
const isLoadingMore = data && typeof data[size - 1] === "undefined";
53
const isEmpty = data?.[0]?.length === 0;
54
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);
55
56
const loadMore = () => setSize(size + 1);
57
```
58
59
### SWR Infinite Response
60
61
The return value from `useSWRInfinite` with specialized properties for pagination.
62
63
```typescript { .api }
64
interface SWRInfiniteResponse<Data, Error> {
65
/** Array of page data (undefined if not loaded) */
66
data: Data[] | undefined;
67
/** Error from any page (undefined if no error) */
68
error: Error | undefined;
69
/** Scoped mutate function for infinite data */
70
mutate: SWRInfiniteKeyedMutator<Data>;
71
/** Current number of pages */
72
size: number;
73
/** Function to change the number of pages */
74
setSize: (size: number | ((size: number) => number)) => Promise<Data[] | undefined>;
75
/** True when any page is validating */
76
isValidating: boolean;
77
/** True when loading the first page for the first time */
78
isLoading: boolean;
79
}
80
```
81
82
### Key Loader Function
83
84
Function that generates the key for each page, enabling different pagination strategies.
85
86
```typescript { .api }
87
type SWRInfiniteKeyLoader<Data = any, Args = any> = (
88
index: number,
89
previousPageData: Data | null
90
) => Args | null;
91
```
92
93
**Key Loader Examples:**
94
95
```typescript
96
// Offset-based pagination
97
const getKey = (pageIndex: number, previousPageData: any) => {
98
return `/api/data?offset=${pageIndex * 10}&limit=10`;
99
};
100
101
// Cursor-based pagination
102
const getKey = (pageIndex: number, previousPageData: any) => {
103
if (previousPageData && !previousPageData.nextCursor) return null;
104
if (pageIndex === 0) return "/api/data?limit=10";
105
return `/api/data?cursor=${previousPageData.nextCursor}&limit=10`;
106
};
107
108
// Page number pagination
109
const getKey = (pageIndex: number, previousPageData: any) => {
110
if (previousPageData && previousPageData.isLastPage) return null;
111
return `/api/data?page=${pageIndex + 1}`;
112
};
113
114
// Conditional loading (stop when empty)
115
const getKey = (pageIndex: number, previousPageData: any) => {
116
if (previousPageData && !previousPageData.length) return null;
117
return `/api/data?page=${pageIndex}`;
118
};
119
120
// Complex key with multiple parameters
121
const getKey = (pageIndex: number, previousPageData: any) => {
122
return ["/api/search", query, pageIndex, filters];
123
};
124
```
125
126
### Configuration Options
127
128
Extended configuration options specific to infinite loading.
129
130
```typescript { .api }
131
interface SWRInfiniteConfiguration<Data = any, Error = any>
132
extends Omit<SWRConfiguration<Data[], Error>, 'fetcher'> {
133
/** Initial number of pages to load (default: 1) */
134
initialSize?: number;
135
/** Revalidate all pages when any page revalidates (default: false) */
136
revalidateAll?: boolean;
137
/** Keep page size when key changes (default: false) */
138
persistSize?: boolean;
139
/** Revalidate first page on mount and focus (default: true) */
140
revalidateFirstPage?: boolean;
141
/** Load pages in parallel instead of sequentially (default: false) */
142
parallel?: boolean;
143
/** Custom comparison function for page data */
144
compare?: SWRInfiniteCompareFn<Data>;
145
/** Fetcher function for infinite data */
146
fetcher?: SWRInfiniteFetcher<Data> | null;
147
}
148
149
type SWRInfiniteFetcher<Data = any, KeyLoader extends SWRInfiniteKeyLoader<any> = SWRInfiniteKeyLoader<Data>> =
150
KeyLoader extends (...args: any[]) => infer Arg | null
151
? Arg extends null
152
? never
153
: (...args: [Arg]) => Data | Promise<Data>
154
: never;
155
156
interface SWRInfiniteCompareFn<Data> {
157
(a: Data | undefined, b: Data | undefined): boolean;
158
}
159
```
160
161
**Configuration Examples:**
162
163
```typescript
164
// Load 3 pages initially
165
const { data } = useSWRInfinite(getKey, fetcher, {
166
initialSize: 3
167
});
168
169
// Parallel page loading
170
const { data } = useSWRInfinite(getKey, fetcher, {
171
parallel: true
172
});
173
174
// Revalidate all pages on focus
175
const { data } = useSWRInfinite(getKey, fetcher, {
176
revalidateAll: true,
177
revalidateOnFocus: true
178
});
179
180
// Persist page size across key changes
181
const { data } = useSWRInfinite(getKey, fetcher, {
182
persistSize: true
183
});
184
185
// Custom page comparison
186
const { data } = useSWRInfinite(getKey, fetcher, {
187
compare: (a, b) => JSON.stringify(a) === JSON.stringify(b)
188
});
189
```
190
191
### Size Management
192
193
Control the number of pages loaded with the `setSize` function.
194
195
```typescript { .api }
196
setSize: (size: number | ((size: number) => number)) => Promise<Data[] | undefined>;
197
```
198
199
**Size Management Examples:**
200
201
```typescript
202
const { data, size, setSize } = useSWRInfinite(getKey, fetcher);
203
204
// Load more pages
205
const loadMore = () => setSize(size + 1);
206
207
// Load specific number of pages
208
const loadExact = (pageCount: number) => setSize(pageCount);
209
210
// Function-based size update
211
const doublePages = () => setSize(current => current * 2);
212
213
// Reset to first page
214
const reset = () => setSize(1);
215
216
// Load all available pages (be careful!)
217
const loadAll = () => {
218
// Keep loading until no more data
219
let currentSize = size;
220
const loadNext = () => {
221
setSize(currentSize + 1).then((newData) => {
222
if (newData && newData[currentSize]?.length > 0) {
223
currentSize++;
224
loadNext();
225
}
226
});
227
};
228
loadNext();
229
};
230
```
231
232
### Infinite Mutate Function
233
234
Specialized mutate function for infinite data structures.
235
236
```typescript { .api }
237
interface SWRInfiniteKeyedMutator<Data> {
238
/**
239
* Mutate infinite data with support for page-level updates
240
* @param data - New data array, Promise, or function returning new data
241
* @param options - Mutation options including revalidation control
242
* @returns Promise resolving to the new data array
243
*/
244
(
245
data?: Data[] | Promise<Data[]> | ((currentData: Data[] | undefined) => Data[] | undefined),
246
options?: boolean | SWRInfiniteMutatorOptions<Data>
247
): Promise<Data[] | undefined>;
248
}
249
250
interface SWRInfiniteMutatorOptions<Data = any> {
251
/** Whether to revalidate after mutation (default: true) */
252
revalidate?: boolean;
253
/** Control which pages to revalidate */
254
revalidate?: boolean | ((pageData: Data, pageArg: any) => boolean);
255
}
256
```
257
258
**Infinite Mutate Examples:**
259
260
```typescript
261
const { data, mutate } = useSWRInfinite(getKey, fetcher);
262
263
// Update all pages
264
await mutate([...newPagesData]);
265
266
// Add new item to first page
267
await mutate((currentData) => {
268
if (!currentData) return undefined;
269
return [
270
[newItem, ...currentData[0]],
271
...currentData.slice(1)
272
];
273
});
274
275
// Remove item from all pages
276
await mutate((currentData) => {
277
if (!currentData) return undefined;
278
return currentData.map(page =>
279
page.filter(item => item.id !== removedItemId)
280
);
281
});
282
283
// Optimistic update for new page
284
await mutate(
285
[...(data || []), optimisticNewPage],
286
{ revalidate: false }
287
);
288
```
289
290
### Advanced Patterns
291
292
Common patterns for complex infinite loading scenarios.
293
294
**Load More Button:**
295
296
```typescript
297
function InfiniteList() {
298
const { data, error, size, setSize, isValidating } = useSWRInfinite(
299
getKey,
300
fetcher
301
);
302
303
const issues = data ? data.flat() : [];
304
const isLoadingMore = data && typeof data[size - 1] === "undefined";
305
const isEmpty = data?.[0]?.length === 0;
306
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);
307
308
return (
309
<div>
310
{issues.map(issue => (
311
<div key={issue.id}>{issue.title}</div>
312
))}
313
314
{error && <div>Error: {error.message}</div>}
315
316
<button
317
disabled={isLoadingMore || isReachingEnd}
318
onClick={() => setSize(size + 1)}
319
>
320
{isLoadingMore ? "Loading..." :
321
isReachingEnd ? "No more data" : "Load more"}
322
</button>
323
</div>
324
);
325
}
326
```
327
328
**Infinite Scroll:**
329
330
```typescript
331
function InfiniteScroll() {
332
const { data, setSize, size } = useSWRInfinite(getKey, fetcher);
333
const [isIntersecting, setIsIntersecting] = useState(false);
334
const loadMoreRef = useRef(null);
335
336
useEffect(() => {
337
const observer = new IntersectionObserver(
338
([entry]) => setIsIntersecting(entry.isIntersecting),
339
{ threshold: 1 }
340
);
341
342
if (loadMoreRef.current) {
343
observer.observe(loadMoreRef.current);
344
}
345
346
return () => observer.disconnect();
347
}, []);
348
349
useEffect(() => {
350
if (isIntersecting) {
351
setSize(size + 1);
352
}
353
}, [isIntersecting, setSize, size]);
354
355
const items = data ? data.flat() : [];
356
357
return (
358
<div>
359
{items.map(item => (
360
<div key={item.id}>{item.title}</div>
361
))}
362
<div ref={loadMoreRef} />
363
</div>
364
);
365
}
366
```
367
368
**Search with Infinite Results:**
369
370
```typescript
371
function InfiniteSearch() {
372
const [query, setQuery] = useState("");
373
374
const getKey = (pageIndex: number, previousPageData: any) => {
375
if (!query) return null;
376
if (previousPageData && !previousPageData.length) return null;
377
return `/api/search?q=${query}&page=${pageIndex}`;
378
};
379
380
const { data, setSize, size } = useSWRInfinite(getKey, fetcher);
381
382
// Reset pages when query changes
383
useEffect(() => {
384
setSize(1);
385
}, [query, setSize]);
386
387
const results = data ? data.flat() : [];
388
389
return (
390
<div>
391
<input
392
value={query}
393
onChange={(e) => setQuery(e.target.value)}
394
placeholder="Search..."
395
/>
396
397
{results.map(result => (
398
<div key={result.id}>{result.title}</div>
399
))}
400
401
{query && (
402
<button onClick={() => setSize(size + 1)}>
403
Load more results
404
</button>
405
)}
406
</div>
407
);
408
}
409
```