Infinite Loading Concepts

Purpose and Scope

Infinite loading is SWR's pattern for fetching a list that is split across pages, cursors, offsets, or any other sequence of request keys. Instead of forcing an application to manage an array of independent useSWR calls, the swr/infinite entrypoint exposes useSWRInfinite, a hook that treats the sequence as one logical data source. The caller supplies a page-key loader, and the hook returns an array of page results plus controls for increasing or changing the active page count. This page explains the mental model behind that API, how it maps to the source, and how the repository examples demonstrate both load-more buttons and scroll-driven loading.

Sources: src/infinite/index.ts, src/infinite/types.ts, test/use-swr-infinite.test.tsx

The most important shift from ordinary SWR usage is that the key is no longer a single identifier. A page-key loader receives the page index and the previous page's data, then returns the request arguments for that page. This lets applications express simple indexed pagination, offset APIs, cursor APIs, and termination conditions. In the visible tests, basic pages are keyed by strings such as a page number, tuple keys such as a generated key plus index, and API-like URLs containing offsets. The returned data is an array, so rendering commonly joins, maps, or flattens page results.

Sources: src/infinite/types.ts, test/use-swr-infinite.test.tsx

SWR still follows the same stale-while-revalidate foundation: cached page data can render first, and the hook can later revalidate to refresh it. Infinite loading adds list-level metadata on top of that model. The implementation derives a serialized first-page key and prefixes it with an internal infinite marker so the hook can store metadata such as the active page length separately from each page's normal cache entry. That distinction is why changing size, preserving size, and revalidating all pages can be coordinated without making every page key carry list-control state.

Sources: src/infinite/index.ts

Relevant Source Files

  • src/infinite/index.ts - Implements the infinite middleware around the core SWR hook, exports unstable_serialize, computes the first-page metadata key, tracks size, and reads configuration such as initialSize, revalidateAll, persistSize, revalidateFirstPage, revalidateOnMount, and parallel.
  • src/infinite/types.ts - Defines the public TypeScript contract for SWRInfiniteKeyLoader, SWRInfiniteFetcher, SWRInfiniteConfiguration, SWRInfiniteResponse, setSize, and the infinite-specific mutator options.
  • test/use-swr-infinite.test.tsx - Exercises rendering the first page, growing the list with setSize, initialSize, bound mutation, erroring key loaders, cursor-style APIs, and other runtime behaviors for useSWRInfinite.
  • test/use-swr-infinite-preload.test.tsx - Verifies that preload works with the first infinite page, avoids duplicate fetcher calls, supports effect-driven preloading, and integrates with Suspense scenarios covered by the test suite.
  • examples/infinite/README.md - Documents the official load-more example and its local setup commands.
  • examples/infinite-scroll/README.md - Documents the official IntersectionObserver-based infinite scroll example and its local setup commands.

Core Primitives

The public primitive is useSWRInfinite from swr/infinite. It accepts a page-key loader, an optional fetcher, and optional infinite configuration. Its response extends the familiar SWR response shape but changes the data shape to an array of page values and adds a numeric size, a setSize function, and an infinite-aware mutate. The type definitions make that contract explicit: SWRInfiniteResponse omits the normal bound mutator and replaces it with one that works over the page array, while setSize accepts either a number or an updater function.

Sources: src/infinite/types.ts

The page-key loader is the reader-facing center of the API. Its signature receives the zero-based page index and the previous page data, which is null for the first page. Returning request arguments allows the fetcher type to infer its input from the loader's return value. Returning a falsey or not-ready value is the usual way to stop requesting more pages or wait until prerequisites exist. The implementation also guards the first-page serialization in a try block, which allows a not-ready key loader to avoid crashing the hook while dependencies are still unavailable.

Sources: src/infinite/index.ts, src/infinite/types.ts, test/use-swr-infinite.test.tsx

Configuration tunes the list-level behavior. initialSize controls how many pages are requested on first render, defaulting to one in the implementation. persistSize decides whether the active page count should survive when the first page key changes. revalidateAll asks the hook to refresh every page rather than only selected pages. revalidateFirstPage keeps the first page fresh when later pages are involved. revalidateOnMount makes mount-time revalidation explicit for the infinite hook, and parallel is exposed for loading pages without strict dependence on previous-page data when the application's key scheme allows it.

Sources: src/infinite/index.ts, src/infinite/types.ts

mutate is still part of the story, but the shape is list-oriented. The infinite mutator accepts either new page-array data, a promise, or a callback, and its options can include a revalidate property that is either a boolean or a function applied at the page level. This gives callers a way to refresh the whole list after a cache write or be more selective when only some pages need refetching. In the tests, a bound mutate reloads a three-page list after one item in the mock backing data changes, proving that list refresh is not limited to the visible page.

Sources: src/infinite/types.ts, test/use-swr-infinite.test.tsx

Execution Flow

A typical load-more screen starts with a page-key loader that maps index zero to the first API request. On initial render, the hook resolves the active size from cached metadata when available or from initialSize when not. It subscribes to the cache entry for the infinite metadata key using useSyncExternalStore, so changes to size can be observed consistently by React. The hook also keeps a ref for the last page size, which is used when a key change occurs and persistSize is enabled.

Sources: src/infinite/index.ts

When the user asks for more data, the component calls setSize(size + 1) or passes an updater. The tests show this pattern inside an effect that increments the list until three pages have loaded, and a real UI usually does the same from a button click, scroll threshold, or route transition. After the size grows, the hook resolves additional page keys, invokes the fetcher for missing or revalidated pages, and returns the resulting array. Components should render the array defensively because individual page values arrive asynchronously and the initial render may have no data yet.

Sources: test/use-swr-infinite.test.tsx, src/infinite/types.ts

Cursor and offset APIs use the same mechanism with different key-loader logic. For an offset API, the loader can produce a URL containing the current offset based on the page index. For a cursor API, the loader can inspect the previous page and return the next cursor, or stop when the previous page indicates there is no next page. This is why the loader receives previous page data: the hook does not need to understand a backend's pagination protocol. It only needs a stable sequence of keys and a fetcher that knows how to resolve those keys.

Sources: src/infinite/types.ts, test/use-swr-infinite.test.tsx

The load-more example in the repository is intentionally small: it exists to show useSWRInfinite with a button that requests more data. The infinite-scroll example demonstrates the same hook with an IntersectionObserver-based trigger instead of an explicit button. These are interface choices around the same data primitive. A button makes the active size a deliberate user action, while an observer can call the same size control when a sentinel element becomes visible near the bottom of the list.

Sources: examples/infinite/README.md, examples/infinite-scroll/README.md

Preloading and Page Readiness

Preloading is useful when the next list view or first page is predictable before the component renders. The preload tests call preload from swr with the first page key produced by the infinite key loader and the same fetcher that the hook will later use. When the component mounts, useSWRInfinite consumes that resource and returns page-array data without calling the fetcher again. The tests also verify that repeated preload calls for the same key are deduplicated, which protects hover handlers, effects, and route transitions from accidentally issuing duplicate requests.

Sources: test/use-swr-infinite-preload.test.tsx

The same test file covers effect-driven preloading, where a parent component preloads the first page before conditionally showing the infinite child. That pattern is a practical bridge between navigation intent and render-time data needs. It lets the application start work during an earlier interaction and still keep the actual list component simple. Suspense coverage also appears in the preload tests, showing that preloaded infinite data can participate in Suspense render paths and reduce unnecessary fallback work when the resource has already been requested.

Sources: test/use-swr-infinite-preload.test.tsx

Preloading infinite lists does not mean preloading every page. The visible tests focus on the first page because that is the page the hook needs to render the initial list state. Applications can extend the idea by preloading likely next-page keys when they know them, but cursor-based APIs often cannot know the next key until the previous page has arrived. In those cases, prefer a conservative preload strategy: preload the first page for navigation speed, then let setSize and the key loader drive subsequent pages from actual response data.

Sources: src/infinite/types.ts, test/use-swr-infinite-preload.test.tsx

Example Workflows

To run the official load-more example locally, download only the example directory, install dependencies, and start the development server. The README presents both Yarn and npm commands. The documented idea is direct: use useSWRInfinite with a load-more data button. Use this example when you want to understand the smallest complete application shape before adding route state, virtualization, optimistic mutation, or custom page rendering.

Sources: examples/infinite/README.md

curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/infinite
cd basic
yarn
yarn dev
# or
npm install
npm run dev

The infinite-scroll example has the same setup style but frames the interaction around IntersectionObserver. That detail matters because the data model does not change when the trigger changes. The observer only decides when to call the size control; useSWRInfinite still owns the page array, cache coordination, and revalidation behavior. This separation is a useful design guideline for production lists: keep viewport observation, button state, and loading indicators in the component layer, and keep page identity in the key loader.

Sources: examples/infinite-scroll/README.md

curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/infinite-scroll
cd infinite-scroll
yarn
yarn dev
# or
npm install
npm run dev

System-to-Code Mapping

ConceptSource-level nameWhat it controls
Page identitySWRInfiniteKeyLoaderMaps page index and previous page data to request arguments.
Page fetchSWRInfiniteFetcherResolves each key-loader return value into page data.
Active page countsize and setSizeReads and changes how many pages belong to the list.
Initial page countinitialSizeSets the first render's requested number of pages when no cached size exists.
Size persistencepersistSizeDecides whether page count survives first-key changes.
Full-list refreshrevalidateAll and mutateControls whether cache writes and refreshes affect all pages.
First-page freshnessrevalidateFirstPageKeeps the first page involved in revalidation decisions.
Preloaded first pagepreload(getKey(0), fetcher)Starts the first page request before the infinite component renders.

Testing Signals and Next Steps

The unit tests are valuable reading because they encode user-visible guarantees rather than only implementation details. They confirm that the first page renders as an array response, that setSize can grow a list across several pages, that initialSize can request multiple pages immediately, and that a throwing key loader does not make a later bound mutate crash the component. The preload tests add another guarantee: preloading the first page should produce the same data result, should not repeatedly call the fetcher for the same resource, and can be initiated from an effect before the list appears.

Sources: test/use-swr-infinite.test.tsx, test/use-swr-infinite-preload.test.tsx

After learning the concept, read the useSWRInfinite API reference for option-by-option details and TypeScript signatures, then inspect the example pages for concrete UI patterns. If your list supports manual refresh, also read the mutation concepts and mutate reference, because infinite lists often need to update or revalidate cached pages after creating, deleting, or editing an item. If your list is route-driven, pair this page with cache and key serialization guidance so page keys stay stable across navigation and re-rendering.