TanStack Query Infinite Cursor Pagination

Drive cursor-based infinite lists with TanStack Query useInfiniteQuery, threading nextCursor through pages and prefetching ahead of scroll.

TypescriptReactData Fetching

Code

snippet
import {
  useInfiniteQuery, queryClient, keepPreviousData,
} from '@tanstack/react-query';

interface FeedPage {
  items: { id: string; title: string }[];
  nextCursor: string | null;
}

async function fetchFeed(cursor?: string): Promise<FeedPage> {
  const params = new URLSearchParams({ limit: '20' });
  if (cursor) params.set('cursor', cursor);

  const res = await fetch('/api/feed?' + params.toString());
  if (!res.ok) throw new Error('feed failed: ' + res.status);
  return res.json();
}

function useFeed() {
  return useInfiniteQuery({
    queryKey: ['feed'],
    queryFn: ({ pageParam }) => fetchFeed(pageParam),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    staleTime: 30_000,
    placeholderData: keepPreviousData,
  });
}

function FeedList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeed();
  const items = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <section>
      {items.map((item) => <article key={item.id}>{item.title}</article>)}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'loading…' : 'load more'}
      </button>
    </section>
  );
}

queryClient.prefetchInfiniteQuery({
  queryKey: ['feed'],
  queryFn: ({ pageParam }) => fetchFeed(pageParam),
  initialPageParam: undefined as string | undefined,
  pages: 1,
});