Load More with useSWRInfinite

Purpose and Scope

This page explains the load-more pagination pattern built around useSWRInfinite, the SWR hook exported from the swr/infinite entrypoint. The goal is to help you implement a button that requests the next page only when the user asks for it, while keeping SWR's cache, revalidation, and loading-state behavior intact. The official example describes this scenario as using useSWRInfinite with a load-more data button; the source-backed API surface shows that the same hook also supports Suspense, preloading, typed page keys, and page-size mutation.

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

SWR's base model is cache-first data fetching: a component receives cached data when available, then SWR revalidates and updates the UI with fresh data. For paginated lists, that model becomes a sequence of cached page responses rather than one response object. useSWRInfinite keeps those page responses as an array, exposes the current size, and provides setSize so the UI can ask SWR to resolve more page keys and fetch additional pages. That is the core difference between a normal useSWR list and a load-more list.

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

Relevant Source Files

  • src/infinite/types.ts - Defines the public TypeScript contract for SWRInfiniteKeyLoader, SWRInfiniteFetcher, SWRInfiniteConfiguration, SWRInfiniteResponse, setSize, and the infinite-specific mutate signature.
  • src/infinite/index.ts - Implements the infinite middleware around the core SWR hook, stores the page-size metadata, exports unstable_serialize, and wires cache subscriptions for page-size updates.
  • README.md - Provides the repository's top-level explanation of SWR as a React Hooks data-fetching library and frames cache, revalidation, pagination, Suspense, and TypeScript as first-class capabilities.
  • infinite/package.json - Declares the private workspace package metadata that points the swr/infinite subpackage to the built CommonJS, ESM, and declaration outputs.
  • e2e/site/app/render-suspense-infinite-preload/page.tsx - Exercises useSWRInfinite with preload and React Suspense in a client component, showing that the first page can be prepared before render.
  • e2e/site/app/suspense-infinite-get-key/page.tsx - Exercises useSWRInfinite with Suspense, a changing key loader, and setSize(1) when switching list identity.

Core Primitives

A load-more implementation has three moving pieces. The first is the key loader, typed as SWRInfiniteKeyLoader, which receives the page index and the previous page's data. It returns the key or arguments for that page. The second is the fetcher, typed as SWRInfiniteFetcher, whose input is inferred from the key loader's return value unless the loader returns null, false, or undefined. The third is the response object, SWRInfiniteResponse, which extends the normal SWR response with size, setSize, and an infinite-aware mutate.

Sources: src/infinite/types.ts

The key loader is the part that turns a user action into page identity. A simple load-more list often uses the page index directly, such as /api/items?page=${index + 1}. A cursor-based list can inspect previousPageData and stop by returning null when there are no more records. The type definition deliberately passes previousPageData as Data | null, so the first page can be detected without special cache access and later pages can depend on the server response from the page before them.

Sources: src/infinite/types.ts

The response shape is intentionally array-oriented. data is Data[] | undefined through the inherited SWR response, where each element is one page, not one row. Rendering usually flattens the array before mapping records. size is the number of pages SWR is currently tracking. setSize accepts either a number or an updater function, returns a promise for the new page array, and is the public API a button uses to request another page. The bound mutate can update all pages or revalidate selected pages with an infinite-specific predicate.

Sources: src/infinite/types.ts

import useSWRInfinite from 'swr/infinite'
 
type Page = { items: string[]; nextCursor?: string }
 
const fetcher = (url: string) => fetch(url).then(res => res.json() as Promise<Page>)
 
function LoadMoreList() {
  const getKey = (index: number, previousPageData: Page | null) => {
    if (previousPageData && !previousPageData.nextCursor) return null
    if (index === 0) return '/api/items'
    return `/api/items?cursor=${previousPageData?.nextCursor}`
  }
 
  const { data, error, isLoading, size, setSize } = useSWRInfinite(getKey, fetcher)
  const items = data ? data.flatMap(page => page.items) : []
 
  if (error) return <p>failed to load</p>
  if (isLoading) return <p>loading first page...</p>
 
  return (
    <>
      <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>
      <button type="button" onClick={() => void setSize(size + 1)}>
        load more
      </button>
    </>
  )
}

Load-More Execution Flow

When a component calls useSWRInfinite, the implementation wraps the normal useSWR hook as middleware rather than replacing the core cache and revalidation machinery. The implementation computes a serialized key for the first page and prefixes it with SWR's infinite marker so metadata for this hook can live in the cache separately from ordinary page responses. That metadata includes _l, the stored page size. If no cached page size exists, the hook uses initialSize, which defaults to one page.

Sources: src/infinite/index.ts

The load-more button works by changing page size, not by manually firing a fetch for an arbitrary URL. Internally, setSize updates the infinite metadata so the hook resolves more page keys and lets SWR fetch the missing page data through the configured fetcher. This distinction matters because cache subscriptions, deduplication, revalidation, and Suspense behavior remain coordinated through SWR. The hook also subscribes to the cached infinite metadata with useSyncExternalStore, so components can observe size changes consistently across React rendering modes.

Sources: src/infinite/index.ts

The implementation supports several configuration choices that change the user experience of the button. initialSize controls how many pages are requested on first render. persistSize determines whether a changed first-page key should keep the previous page count or reset to the cached initial size. revalidateFirstPage, revalidateAll, revalidateOnMount, and parallel influence which pages are revalidated and whether page fetches can be handled independently. For a basic button, the defaults usually match expectations: one first page, then one additional page per click.

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

Suspense, Preload, and Key Changes

The E2E App Router pages show that the infinite hook participates in the same Suspense and preload paths as the rest of SWR. In render-suspense-infinite-preload, the page defines a base key, preloads the first page with preload(getKey(0), fetcher), and then renders useSWRInfinite(index => getKey(index), fetcher, { suspense: true }) inside a Suspense boundary. The test page tracks fetch and fallback counts, demonstrating the intended behavior for a first page that can be prepared before the component reads it.

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

Another E2E page demonstrates a subtle but important load-more edge case: the list identity can change. The component's key loader returns one of two fixed keys based on local state, uses Suspense, and calls setSize(1) after switching from status a to status b. This mirrors a real UI where a search filter, tab, or account selector changes the list. Resetting the size prevents a newly selected list from inheriting an old page count when the UI should return to a first-page view.

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

Compact API Reference

PrimitiveSource-backed contractUse in a load-more UI
useSWRInfiniteImported from swr/infinite; implemented as infinite middleware over the core SWR hook.Call it with getKey, an optional fetcher, and optional infinite configuration.
SWRInfiniteKeyLoader`(index: number, previousPageData: Datanull) => Args`.
SWRInfiniteFetcherReceives the key-loader return value and returns Data or Promise<Data>.Fetch one page, not the whole accumulated list.
sizeNumber of pages tracked by the hook.Display or compute the next requested page count.
setSizeAccepts a number or updater and returns `Promise<Data[]undefined>`.
initialSizeInfinite configuration option with default behavior of one initial page.Preload or render more than one page at mount when appropriate.
persistSizeInfinite configuration option.Decide whether a changed first-page key keeps the existing page count.
mutateInfinite keyed mutator with an optional per-page revalidate predicate.Update cached page arrays after local edits or remote mutations.

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

The swr/infinite package entrypoint is a real published subpath. The workspace package metadata maps its built CommonJS file to ../dist/infinite/index.js, its ESM file to ../dist/infinite/index.mjs, and its declarations to ../dist/infinite/index.d.ts. Application code should import from swr/infinite, not from internal source paths, because the package exports are designed around those built entrypoints and associated type declarations.

Sources: infinite/package.json

Implementation Notes and Next Steps

Prefer keeping page keys stable and descriptive. If a key includes search text, filters, or authentication-sensitive parameters, changing those inputs changes the first-page key and therefore the infinite metadata key. Decide whether persistSize matches that experience. For a feed where changing filters should restart at page one, the default reset behavior is helpful. For a layout where the same list identity is preserved across small query changes, persisting size can avoid surprising collapse of already requested pages.

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

For the next implementation step, start with the official load-more example flow: install and run the example app, then adapt the key loader to your API's pagination style. Use setSize as the only button action, flatten data only for rendering, and let SWR own cache updates. After the basic button works, read the API reference for useSWRInfinite, the infinite-scroll example for IntersectionObserver triggering, and the mutation concepts page if users can edit records inside the paginated list.