Infinite queries with TanStack Query
Paginated feeds—social timelines, admin tables, search results—map naturally to useInfiniteQuery: each “page” is a fetch keyed by a cursor or offset, and TanStack Query merges results while preserving per-page cache entries.
Minimal shape
import { useInfiniteQuery } from '@tanstack/react-query';
type Page = { items: { id: string }[]; nextCursor: string | null };
export function useFeed() {
return useInfiniteQuery({
queryKey: ['feed'],
initialPageParam: null as string | null,
queryFn: async ({ pageParam }) => {
const url = new URL('/api/feed', window.location.origin);
if (pageParam) url.searchParams.set('cursor', pageParam);
const res = await fetch(url);
if (!res.ok) throw new Error('feed failed');
return res.json() as Promise<Page>;
},
getNextPageParam: (last) => last.nextCursor,
});
}
Render with data.pages.flatMap((p) => p.items) or keep nested structure if you need per-page metadata.
Stable queryKey
Include every variable that changes the result set: ['feed', { filter, sort }] not only ['feed']. Otherwise stale pages bleed across user actions.
Mutations and list invalidation
After creating a post, either:
queryClient.invalidateQueries({ queryKey: ['feed'] })(simple, refetches from scratch), orsetQueryDatato prepend optimistically (faster UX, more code).
For infinite lists, setQueryData must respect the pages array shape—utility helpers in the docs flatten this operation.
Bi-directional infinite scroll
getPreviousPageParam enables “load older” above the fold. Combine with virtualized lists (@tanstack/react-virtual) so DOM nodes stay bounded.
Errors and retries
Use maxPages (v5) or manual guards to cap memory if the user scrolls forever. Surface fetchNextPage errors inline; do not swallow partial failures silently.
Summary
useInfiniteQuery is the right primitive when the server speaks cursors or numbered pages and the UI stacks them. Invest in queryKey hygiene and a clear invalidation story when mutations touch the same list.