TanStack QueryPaginationTypeScriptReact

TanStack Query: Infinite Queries and Cursor Pagination

Implement efficient infinite scrolling and cursor-based pagination with TanStack Query, covering useInfiniteQuery patterns, prefetching, and data synchronization.

Sam Rivera
Sam Rivera
June 12, 202611 min read

TL;DR: TanStack Query's useInfiniteQuery handles cursor and offset pagination with built-in state management for page accumulation, direction, and prefetching. This guide covers cursor-based pagination with TanStack Start server functions, reverse pagination for chat-like UIs, stale-while-revalidate for infinite lists, and optimistic prepending for real-time updates.


Introduction

Pagination is deceptively complex. Offset pagination breaks when items are added or removed. Cursor pagination requires careful state management. Infinite scroll has edge cases with prefetching, race conditions, and memory growth.

TanStack Query's useInfiniteQuery abstracts most of this complexity. It manages page accumulation, provides fetchNextPage and fetchPreviousPage callbacks, and integrates naturally with TanStack Router's preloading system.


Cursor Pagination with Server Functions

tsx
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { db } from '../db'

export const listProductsFn = createServerFn({ method: 'GET' })
  .validator(z.object({
    cursor: z.string().optional(),
    limit: z.number().default(20),
  }))
  .handler(async ({ data }) => {
    const { cursor, limit } = data
    const products = await db
      .selectFrom('products')
      .orderBy('createdAt', 'desc')
      .limit(limit + 1)
      .cursor(cursor ? { after: cursor } : undefined)
      .execute()

    const hasMore = products.length > limit
    const items = hasMore ? products.slice(0, limit) : products
    const nextCursor = hasMore ? items[items.length - 1].id : null

    return { items, nextCursor }
  })

useInfiniteQuery Hook

tsx
import { useInfiniteQuery } from '@tanstack/react-query'
import { listProductsFn } from '../server/products'

function ProductList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: ['products'],
      queryFn: ({ pageParam }) =>
        listProductsFn({ data: { cursor: pageParam } }),
      initialPageParam: undefined,
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    })

  return (
    <div>
      {data?.pages.map((page) =>
        page.items.map((product) => <ProductCard key={product.id} product={product} />)
      )}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  )
}

Infinite Scroll with Intersection Observer

tsx
function InfiniteProductList() {
  const observerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage()
        }
      },
      { threshold: 0.1 }
    )
    if (observerRef.current) observer.observe(observerRef.current)
    return () => observer.disconnect()
  }, [fetchNextPage, hasNextPage, isFetchingNextPage])

  return (
    <div>
      {items}
      <div ref={observerRef} />
    </div>
  )
}

Pagination Strategy Comparison

StrategyProsConsBest For
Offset (LIMIT/OFFSET)Simple, skip to page NBreaks on insert/deleteStatic datasets
Cursor (keyset)Stable, fast on large dataCannot skip pagesDynamic feeds
Time-basedNatural orderingUneven page sizesActivity streams
Search-after (elastic)Works with full-textDB-specificSearch results

Conclusion

useInfiniteQuery with cursor-based pagination provides the best UX for dynamic data feeds. Combined with TanStack Start server functions, you get type-safe pagination from database to UI with minimal boilerplate.

For server-side pagination patterns, see TanStack Query: Server Functions Best Practices. Master cache management with Cache Invalidation Strategies. If you are rendering large paginated lists, TanStack Virtual Scrolling pairs naturally with infinite queries, and the SaaS Database Architecture guide covers the underlying data structures for efficient pagination.