Skip to content

Pagination ​

Cursor, offset/limit, and page-number pagination are not three APIs. They are three implementations of one function:

ts
getNextPageParam(lastPage, allPages, lastPageParam, allPageParams)
  => TPageParam | null | undefined // null/undefined = no more pages

The engine never branches on a pagination "strategy" — the strategy is data you supply. That's what keeps the core tiny and lets pagination shapes nobody anticipated work anyway.

Cursor ​

The server hands back the next cursor; you return it.

ts
interface Page {
  items: Item[]
  nextCursor: string | null
}

createInfiniteScroll<Page, string>({
  initialPageParam: 'start',
  fetchPage: async ({ pageParam, signal }) =>
    (await fetch(`/api/items?cursor=${pageParam}`, { signal })).json(),
  getNextPageParam: (last) => last.nextCursor, // null ends it
})

Offset / limit ​

There is no cursor in the response, so derive the next offset from the last param — and stop when a short page proves you've hit the end.

ts
const LIMIT = 20

createInfiniteScroll<Item[], number>({
  initialPageParam: 0,
  fetchPage: async ({ pageParam, signal }) =>
    (await fetch(`/api/items?offset=${pageParam}&limit=${LIMIT}`, { signal })).json(),
  getNextPageParam: (lastPage, _allPages, lastParam) =>
    lastPage.length === LIMIT ? lastParam + LIMIT : null,
})

Page number ​

ts
createInfiniteScroll<Page, number>({
  initialPageParam: 1,
  fetchPage: async ({ pageParam, signal }) =>
    (await fetch(`/api/items?page=${pageParam}`, { signal })).json(),
  getNextPageParam: (last, _allPages, lastParam) =>
    lastParam < last.totalPages ? lastParam + 1 : null,
})

0 is a valid page param ​

The engine checks == null, never truthiness. Offset 0, page 0, and an empty string cursor are all real params that fetch real pages; only null and undefined end the list. There's a regression test for exactly this in pagination.test.ts.

ts
// Correct — ends only when there is genuinely nothing left.
getNextPageParam: (last) => last.nextOffset ?? null

// Wrong — offset 0 silently terminates the list.
getNextPageParam: (last) => last.nextOffset || null

The arguments you rarely need ​

All four parameters are there so you never have to keep state outside the engine:

ParameterUse it when
lastPagethe response carries the next cursor (the common case)
allPagesthe count of loaded pages is the next param (allPages.length)
lastPageParamyou're incrementing an offset or page number
allPageParamsyou need the full history — deduping, or a time-window walk
ts
// "Next page number is however many pages I already have, plus one."
getNextPageParam: (last, allPages) => (last.hasMore ? allPages.length + 1 : null)

Reading the pages ​

pages is an array of whatever fetchPage returned, in load order. Flatten it at render time:

ts
const items = pages.flatMap((page) => page.items)

pageParams is the parallel array of the params used to fetch each page — handy for debugging, and for refetch-style logic you build yourself.

Bi-directional pagination ​

Not supported yet. getPreviousPageParam is on the roadmap; today the engine only walks forward. If you need "load older messages above", track the backward cursor yourself and prepend, or run a second engine for the other direction.