useSWRInfinite
Purpose and Scope
useSWRInfinite is SWR’s public hook for paginated and infinite-list data fetching. The core useSWR hook models one cache key and one response value; useSWRInfinite raises that model to a sequence of page keys and returns an array of page results. That makes it the right entrypoint for “load more” buttons, scroll-based pagination, and list UIs where each page has its own request identity but the component wants a single combined state surface. SWR’s README frames the library around cached data, revalidation, pagination, and scroll position recovery, and the infinite hook is the dedicated API for the pagination part of that story.
Sources: README.md, src/infinite/types.ts
Import the hook from the swr/infinite subpath rather than from the root package. The package shim for the subpath points CommonJS, ESM, and TypeScript consumers at the built infinite bundle and declaration file, while the implementation source exports the infinite middleware behavior and the serialization helper used by this hook family. In application code, the E2E fixtures demonstrate the intended consumer import shape with import useSWRInfinite from 'swr/infinite'. That import gives the component a hook with page-size controls, page-array data, and an infinite-specific mutate function.
Sources: infinite/package.json, src/infinite/index.ts, e2e/site/app/render-suspense-infinite-preload/page.tsx
Conceptually, useSWRInfinite still follows SWR’s stale-while-revalidate model: render from cache when possible, request fresh data through a fetcher, and update React when the request resolves. The difference is that the key is produced by a key loader function for every page. The hook calls that loader with the current page index and the previous page’s data, allowing cursor-style APIs to stop when there is no next page or build a URL from data returned by the preceding page. The TypeScript contract names this function SWRInfiniteKeyLoader and defines it as receiving index and previousPageData.
Sources: README.md, src/infinite/types.ts
Relevant Source Files
src/infinite/types.ts— Defines the public TypeScript contract for infinite keys, fetchers, configuration, comparison, mutation options, response shape, and hook overloads.src/infinite/index.ts— Implements the infinite middleware around the core SWR hook, derives the internal infinite metadata key, stores page size in cache state, and exportsunstable_serialize.README.md— Establishes SWR’s stale-while-revalidate model and lists pagination and scroll position recovery among the library’s supported capabilities.infinite/package.json— Provides the subpackage entry metadata forswr/infinite, includingmain,module, andtypespaths into the builtdist/infiniteoutput.e2e/site/app/render-suspense-infinite-preload/page.tsx— ExercisesuseSWRInfinitewithsuspense: trueand a rootpreloadcall for the first page key.e2e/site/app/suspense-infinite-get-key/page.tsx— ExercisesuseSWRInfiniteunder Suspense when the key-producing state changes and the component resets the requested size.
API Components
The key loader is the first argument and is the most important part of the API. Its public type is SWRInfiniteKeyLoader<Data, Args> = (index: number, previousPageData: Data | null) => Args. For the first page, previousPageData is null; for later pages, it can be used to read cursors, detect an empty page, or decide that no further page should be requested. The fetcher type is derived from the key loader: when the key loader returns a concrete key, the infinite fetcher receives that key’s value and returns either data or a promise of data. When the key loader returns null, false, or undefined, the conditional type prevents a normal fetcher call for that unresolved page key.
Sources: src/infinite/types.ts
The response shape extends the normal SWR response but replaces the single data value with a page array and adds explicit size controls. SWRInfiniteResponse<Data, Error> omits the base mutate and returns size, setSize, and an infinite-aware mutate. The data property, inherited through the response contract, is represented as Data[], where each array element is the result for one page. setSize accepts either a numeric page count or an updater function that receives the current size, and it resolves to the updated array of pages when the operation completes.
Sources: src/infinite/types.ts
The configuration type is SWRInfiniteConfiguration<Data, Error, Fn>. It starts from the normal SWRConfiguration<Data[], Error> but omits compare so the infinite hook can define a comparison function that understands both individual pages and arrays of pages. Infinite-specific options include initialSize, revalidateAll, persistSize, revalidateFirstPage, and parallel. The configuration may also carry an infinite-specific fetcher, and its compare callback can compare page data or page arrays. These options let a component choose how many pages to request initially, whether all pages should revalidate, whether size survives key changes, and whether pages may be loaded in parallel.
Sources: src/infinite/types.ts, src/infinite/index.ts
import useSWRInfinite from 'swr/infinite'
const getKey = (index: number, previousPageData: User[] | null) => {
if (previousPageData && previousPageData.length === 0) return null
return `/api/users?page=${index}`
}
function Users() {
const { data, size, setSize, isLoading, error } = useSWRInfinite(getKey, fetcher, {
initialSize: 1,
revalidateFirstPage: true
})
const users = data ? data.flat() : []
return <button onClick={() => setSize(size + 1)}>load more</button>
}Compact Reference
| API | Contract | Notes |
|---|---|---|
useSWRInfinite(getKey) | Uses a key loader and configured/global fetcher | Returns SWRInfiniteResponse<Data, Error>. |
useSWRInfinite(getKey, fetcher) | Uses an explicit infinite fetcher | The fetcher argument is typed from the key loader return value when possible. |
useSWRInfinite(getKey, config) | Uses configuration, including optional fetcher | Useful when options are more important than a positional fetcher. |
useSWRInfinite(getKey, fetcher, config) | Full form | Combines explicit fetcher with infinite and base SWR options. |
SWRInfiniteKeyLoader | (index, previousPageData) => Args | Produces one page key at a time. |
SWRInfiniteFetcher | `(args) => Data | Promise` |
SWRInfiniteResponse.data | `Data[] | undefined` through the response contract |
SWRInfiniteResponse.size | number | Current number of requested pages. |
SWRInfiniteResponse.setSize | `(number | updater) => Promise<Data[] |
SWRInfiniteResponse.mutate | infinite keyed mutator | Supports array data and per-page revalidation selection. |
The infinite mutator is specialized because page data often needs more careful revalidation than a single resource. SWRInfiniteMutatorOptions inherits the base mutator options but replaces revalidate with either a boolean or an infinite revalidation function. That function receives page data and a key and returns whether that page should revalidate. This design supports common workflows such as optimistically appending a newly created item, replacing one page after an edit, or forcing all pages to reload after a filter changes, without requiring every page to be treated the same.
Sources: src/infinite/types.ts
System-to-Code Mapping
The implementation builds the infinite hook as middleware over the normal SWR hook. The exported infinite value accepts a useSWRNext hook, then returns a function that receives the key loader, fetcher, and merged configuration. This is why infinite behavior stays aligned with core SWR behavior: caching, revalidation, and fetcher execution are still rooted in the shared hook machinery, while the middleware is responsible for translating a page sequence into SWR-compatible state. The source comment explicitly notes that useSWRInfinite needs special type casts because its key and return type do not match the normal useSWR types.
Sources: src/infinite/index.ts
To scope metadata for a whole infinite list, the implementation serializes the first page key with getFirstPageKey, prefixes it with INFINITE_PREFIX, and uses that value as the internal metadata key. That metadata key is not just another user page key; it stores hook-level state such as the requested page count. The implementation catches errors while computing the first page key and treats that as a “not ready yet” situation, which matches the broader SWR convention that unresolved keys can delay fetching until enough component state exists.
Sources: src/infinite/index.ts
Page size is stored in cache metadata under _l. The implementation creates cache helpers for the infinite metadata key, derives the current size from _l or initialSize, and subscribes with useSyncExternalStore so React sees size changes consistently. The hook also keeps a ref to the last resolved page size. When the first-page key changes after mount, persistSize controls whether the old size is kept or reset to the size resolved for the new key. This distinction matters for filtered lists: keeping size may preserve a user’s “loaded more” intent, while resetting size avoids requesting too many pages for a new query.
Sources: src/infinite/index.ts
Execution Flow
A typical render starts by calling getKey(0, null) to derive the first page key. If the key is available, the implementation can derive the infinite metadata key, read the cached page size, and begin requesting pages. For sequential pagination, later calls to getKey can use the previous page’s data to derive the next key. That is the pattern needed for cursor APIs, where the second request may not be knowable until the first response arrives. The parallel option exists for workloads where pages do not depend on previous data and can be considered independently.
Sources: src/infinite/types.ts, src/infinite/index.ts
The official examples describe two common reader-facing shapes: a load-more button and infinite scroll based on IntersectionObserver. Both are the same API at the hook level. A load-more button reads size and calls setSize(size + 1) in response to a click. An infinite-scroll component does the same when the sentinel element intersects the viewport. In both cases, data remains an array of pages, so rendering usually involves flattening or mapping nested arrays rather than expecting one object from the hook.
Sources: src/infinite/types.ts
Suspense support is exercised by the E2E pages. In the preload case, the page imports preload from the root swr entrypoint, calls preload(getKey(0), fetcher) before the component renders, and then renders useSWRInfinite(index => getKey(index), fetcher, { suspense: true }) inside a React Suspense boundary. The fixture tracks fetch and fallback counts, which documents the intended interaction: first-page preloading can feed the same key that the infinite hook later asks for, while Suspense controls how loading is presented to the user.
Sources: e2e/site/app/render-suspense-infinite-preload/page.tsx
The second Suspense fixture shows that the key loader may close over React state. It chooses between two string keys based on a local status state, renders the array as stringified data, and has a button that changes the status and calls setSize(1). This is an important operational pattern: when the logical list identity changes, resetting size to one page can keep the next render focused on the new first page rather than carrying a stale page count from the previous list identity. The implementation’s persistSize option controls similar key-change behavior at the hook configuration level.
Sources: e2e/site/app/suspense-infinite-get-key/page.tsx, src/infinite/index.ts
TypeScript and Pagination Patterns
The overloads in SWRInfiniteHook support several calling styles while preserving useful inference. When the key loader returns a strict tuple key, the fetcher can be typed against the tuple. When a less specific key loader is used, the overloads fall back to BareFetcher<Data>. This lets simple URL-based pagination stay concise and still allows advanced tuple-key APIs to keep argument types precise. The same generic parameters flow through data, error, key loader, fetcher, configuration, and response, so a typed page value becomes visible in data, mutate, comparison callbacks, and revalidation callbacks.
Sources: src/infinite/types.ts
A practical rule is to make the key loader express list identity and page identity together. For offset pagination, the key might include a route and an index. For cursor pagination, the key might include a cursor from previousPageData. For filtered search results, include the filter in the first-page key so the internal infinite metadata key changes when the list identity changes. Then choose persistSize intentionally. If preserving the user’s expanded list across nearby key changes is desirable, enable it; if a new key should go back to the first page, rely on reset behavior or call setSize(1) as shown in the E2E Suspense fixture.
Sources: src/infinite/index.ts, e2e/site/app/suspense-infinite-get-key/page.tsx
Next Steps
Use useSWRInfinite when the UI has one logical list made of multiple SWR-backed page requests. Start with initialSize: 1, render data defensively while it is undefined, and add either a button or scroll observer that calls setSize. Add typed keys and fetchers once the page data shape is clear, especially for cursor-based APIs. If the page participates in Suspense or route-level preloading, align the first page key with the key passed to preload so the same cached result can be reused by the infinite hook.
Sources: src/infinite/types.ts, e2e/site/app/render-suspense-infinite-preload/page.tsx
Related pages to read next: infinite-loading-concepts for design patterns, api-preload for preloading behavior, api-mutate for cache writes, and troubleshooting-suspense-and-server-rendering for Suspense-specific edge cases.