React cookbook · Intermediate

Build infinite scroll with TanStack Query

Load more items automatically as the user scrolls a long feed, using cursor pagination.

Approach

Use useInfiniteQuery with the API's next_cursor as getNextPageParam, and an IntersectionObserver on a sentinel element at the bottom of the list to call fetchNextPage(). Keep a visible "Load more" button as an accessible fallback.

Solution

import { useEffect, useRef } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";

type Page = { items: Post[]; next_cursor: string | null };

export function Feed() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({
    queryKey: ["feed"],
    queryFn: async ({ pageParam, signal }) => {
      const url = pageParam ? `/api/feed?cursor=${encodeURIComponent(pageParam)}` : "/api/feed";
      const res = await fetch(url, { signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return (await res.json()) as Page;
    },
    initialPageParam: null as string | null,
    getNextPageParam: (last) => last.next_cursor,
  });

  const sentinel = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const el = sentinel.current;
    if (!el || !hasNextPage) return;
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting && !isFetchingNextPage) fetchNextPage();
    }, { rootMargin: "400px" });
    observer.observe(el);
    return () => observer.disconnect();
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

  if (status === "pending") return <p>Loading…</p>;
  if (status === "error") return <p role="alert">Couldn't load the feed.</p>;

  return (
    <>
      <ul>{data.pages.flatMap((p) => p.items).map((post) => <li key={post.id}>{post.title}</li>)}</ul>
      <div ref={sentinel} />
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? "Loading…" : "Load more"}
        </button>
      )}
    </>
  );
}

Pitfalls

  • Offset-based pages on a changing feed (duplicates/skips).
  • Rendering thousands of DOM nodes — combine with virtualization for very long lists.
  • No keyboard-accessible way to load more.

Variations

  • Use maxPages to cap memory for very long sessions.
  • Restore scroll position when navigating back (keep the query cached).

Further reading: TanStack Query: Infinite queries

Reference: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries

More React recipes