Infinite Scroll

Purpose and Scope

This page explains the infinite-scroll example pattern for SWR: use useSWRInfinite to model a list as a sequence of cached pages, then let an IntersectionObserver call setSize when a sentinel element near the bottom of the list becomes visible. The official example describes the idea as “useSWRInfinite with scroll based on IntersectionObserver,” which means scrolling is not the data-fetching primitive itself. Scrolling is only the user-interface trigger that asks SWR to load one more page. SWR still owns key generation, fetch execution, cache storage, revalidation, and mutation behavior for the paginated resource.

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

SWR’s README frames the library as a React Hooks data-fetching library built around stale-while-revalidate: return cached data first, revalidate in the background, and update the UI when fresh data arrives. Infinite scroll applies that same model repeatedly. Each page has a key, each key can have cached data, and the rendered list is derived from the array of page results. This keeps the UI responsive because already-loaded pages remain visible while SWR fetches the next page, retries, or revalidates according to the hook configuration.

Sources: README.md, src/infinite/types.ts

Relevant Source Files

  • src/infinite/types.ts — Defines the public TypeScript contract for useSWRInfinite, including SWRInfiniteKeyLoader, SWRInfiniteFetcher, SWRInfiniteConfiguration, SWRInfiniteResponse, size, setSize, and the infinite-specific mutate type.
  • src/infinite/index.ts — Implements the infinite middleware that wraps the core useSWR hook, tracks page size in cache metadata, serializes the first page key, and exports unstable_serialize.
  • README.md — Establishes SWR’s stale-while-revalidate behavior, hook-oriented data model, and feature list including pagination and scroll position recovery.
  • infinite/package.json — Describes the swr/infinite subpackage entrypoint with CommonJS, ES module, and TypeScript declaration outputs under dist/infinite.
  • e2e/site/app/render-suspense-infinite-preload/page.tsx — Exercises useSWRInfinite with Suspense and preload, showing that the first page can be preloaded before render and then consumed by the infinite hook.
  • e2e/site/app/suspense-infinite-get-key/page.tsx — Exercises a Suspense infinite hook whose key changes with component state and whose page size is reset with setSize(1).

Core Primitives

The central primitive is useSWRInfinite from the swr/infinite entrypoint. Its first argument is a getKey function, also called a key loader in the types. The key loader receives the numeric page index and the previous page’s data, then returns the arguments for that page request. Returning null, false, or undefined is represented in the fetcher type as a conditional key and is the usual way to stop requesting more pages when the server indicates there is no next page. The fetcher receives the key-loader output and returns either data or a promise of data.

Sources: src/infinite/types.ts, infinite/package.json

The hook returns a normal SWR response shaped for arrays of pages. data is an array where each element corresponds to one loaded page, and error, validation state, and cache behavior follow SWR’s normal mental model. The infinite response adds size, setSize, and an infinite-aware mutate. For infinite scroll, size is the number of pages requested. Calling setSize(size + 1) requests the next page, while calling setSize(current => current + 1) avoids closing over an old value inside observer callbacks or event handlers.

Sources: src/infinite/types.ts

The configuration type adds pagination-specific controls. initialSize defines how many pages are requested at first render. revalidateAll can force all pages to revalidate, while revalidateFirstPage controls whether the first page is included in revalidation behavior. persistSize keeps the current page count when the key changes, and parallel allows pages to be fetched without depending on each previous page’s result. Those options matter for infinite scroll because a list can grow large, so unnecessary page revalidation can affect latency, bandwidth, and scroll smoothness.

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

Task Flow: Building an IntersectionObserver Infinite List

Start by installing and running the official example if you want a working baseline. The documented flow downloads the example directory from the repository archive, enters infinite-scroll, installs dependencies, and starts the development server. The example can be run with Yarn or npm, and it can also be deployed with Vercel. Those commands are example-project workflow, not library API, but they are useful when comparing your implementation against the repository’s intended scroll-based recipe.

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

In application code, define a page key function before rendering the list. A page API might use /api/items?page=${index} or a tuple key such as ['/api/items', index]. If your endpoint returns an empty page or a cursor with no next value, return null from getKey for the next index. That stop condition is important because an IntersectionObserver can fire repeatedly as the sentinel remains visible. The key loader is the gate that tells SWR whether there is another request to make.

Sources: src/infinite/types.ts

A typical observer setup renders all loaded pages, then places a small sentinel element after the list. In an effect, create an IntersectionObserver that watches the sentinel. When the observer reports an intersecting entry, call setSize(current => current + 1). The observer should be disconnected during cleanup so unmounted components do not continue asking for pages. This pattern keeps scroll detection in browser APIs and keeps data concerns in SWR, which is exactly the separation the example is meant to demonstrate.

import useSWRInfinite from 'swr/infinite'
 
const fetcher = (url: string) => fetch(url).then(res => res.json())
 
function Feed() {
  const { data, error, isLoading, size, setSize } = useSWRInfinite(
    index => `/api/items?page=${index}`,
    fetcher
  )
 
  // Attach an IntersectionObserver to a bottom sentinel and call:
  // void setSize(current => current + 1)
  const items = data ? data.flat() : []
  return <>{items.map(item => <article key={item.id}>{item.title}</article>)}</>
}

System-to-Code Mapping

The implementation in src/infinite/index.ts shows why setSize works across components and renders. The infinite middleware computes a serialized key for the first page, prefixes it with the infinite namespace, and stores metadata for that infinite hook in the configured cache. It uses a cache helper and useSyncExternalStore so page-size changes can be observed consistently. The cached _l value represents the loaded page count; if it is absent, the hook falls back to initialSize. This is why setSize behaves like state while still being coupled to SWR cache metadata.

Sources: src/infinite/index.ts

When the first page key changes, the implementation resets or preserves the page size depending on persistSize. That behavior matters in infinite scroll screens with filters, search terms, or user-specific list keys. Without persistence, switching from one feed to another can reset the list back to the cached or initial page count. With persistence, the hook can retain the previous size. The E2E Suspense key-change page demonstrates a state-driven key switch and explicitly calls setSize(1), which is a practical pattern when a UI action should return the infinite list to its first page.

Sources: src/infinite/index.ts, e2e/site/app/suspense-infinite-get-key/page.tsx

The E2E preload page also shows that infinite loading participates in SWR’s broader render lifecycle. It preloads the first page key with preload(getKey(0), fetcher), then renders useSWRInfinite inside Suspense with a matching key loader. For infinite scroll, this means the above-the-fold first page can be warmed before the list component reads it, while later pages still load through setSize as the sentinel appears. The test route tracks fetch count and fallback render count to verify that preloading and Suspense interact with the infinite hook as expected.

Sources: e2e/site/app/render-suspense-infinite-preload/page.tsx, src/infinite/index.ts

Compact API Reference

ComponentContract for infinite scrollSource
useSWRInfinite(getKey)Requests pages using a key loader and a configured or global fetcher.src/infinite/types.ts
useSWRInfinite(getKey, fetcher)Uses an explicit fetcher whose argument type follows the key-loader return type.src/infinite/types.ts
useSWRInfinite(getKey, fetcher, config)Adds infinite options such as initialSize, persistSize, revalidateAll, revalidateFirstPage, and parallel.src/infinite/types.ts
dataArray of page data, commonly flattened before rendering a continuous list.src/infinite/types.ts
sizeCurrent requested page count.src/infinite/types.ts
setSize(sizeOrUpdater)Changes page count and returns a promise of the page-data array or undefined.src/infinite/types.ts
mutate(data, opts)Updates or revalidates the infinite data set with infinite-specific revalidation options.src/infinite/types.ts
unstable_serializeExported from the infinite entrypoint for serializing infinite keys where supported.src/infinite/index.ts

Testing Signals and Practical Checks

A healthy infinite-scroll implementation should make page loading observable in the UI without hiding already-loaded content. Render a loading affordance near the sentinel, disable repeated requests if you know the final page has been reached, and keep error handling close to the list so a failed next page does not erase earlier pages. The README’s data-flow explanation for useSWR still applies: before a request resolves, data can be absent for that page; after resolution, the component rerenders with data or error derived from the fetcher result.

Sources: README.md, src/infinite/types.ts

For Suspense applications, use the E2E routes as behavioral signals. The preload route shows a first page being preloaded before a Suspense-wrapped infinite hook renders. The key-change route shows a Suspense list switching between keys and resetting size through setSize(1). These are not the same as IntersectionObserver, but they validate the same core concerns an infinite-scroll page depends on: stable key generation, page-size updates, Suspense fallback behavior, and safe rerendering when the underlying key changes.

Sources: e2e/site/app/render-suspense-infinite-preload/page.tsx, e2e/site/app/suspense-infinite-get-key/page.tsx

Next Steps

Use this page as the recipe-level bridge between the official infinite-scroll example and the swr/infinite API. If you are implementing a button-driven list instead of scroll detection, read the load-more example next because it uses the same size and setSize primitives with a simpler trigger. If you need exact option semantics, read the useSWRInfinite API reference. If your list uses Suspense or preloads the first page, compare your keys with the E2E preload and key-change routes so the first page, subsequent pages, and reset behavior all line up.