Pagination vs Infinite Scroll in React

Compare pagination and infinite scroll in React. Learn the UX tradeoffs and build each pattern with page state or useInfiniteQuery.

7 min read

Pagination and infinite scroll are two ways to show a long list in React. Pagination splits results into numbered pages, while infinite scroll appends more results as the user reaches the end. This guide compares the tradeoffs and builds each pattern.

The core difference in a table

The two patterns answer the same question differently: how much of a long list to load at once. One replaces the screen, the other extends it. Pagination loads one page and replaces it, while infinite scroll accumulates pages into one growing list.

AspectPaginationInfinite scroll
Data loadedOne page at a timePages accumulate
NavigationNumbered or prev and next buttonsScrolling or Load more
MemoryConstant, one pageGrows with pages loaded
Deep linksEasy with a page queryHarder, needs saved position
Best forSearch results and tablesFeeds and social timelines

The table frames the tradeoff as control versus flow. Pagination gives the user explicit jumps and keeps memory flat, while infinite scroll removes the interruption of clicking but accumulates data in the page. The right choice depends less on the data and more on how the user moves through it.

Build pagination with page state

Pagination keeps the current page in state and folds it into the query key, so changing the page starts a new request.

App.jsxApp.jsx
const [page, setPage] = useState(1);
 
const { data, isPending } = useQuery({
  queryKey: ["posts", page],
  queryFn: () => fetch(`/api/posts?page=${page}`).then((res) => res.json()),
});

The page number is part of the key, so TanStack Query caches each page separately and refetches the right one when the page changes. Keeping page in state also survives re-renders, while the query key ties the cache to that page. This is the key structure from useQuery explained.

App.jsxApp.jsx
<button type="button" onClick={() => setPage((p) => p - 1)} disabled={page === 1}>
  Previous
</button>
<button type="button" onClick={() => setPage((p) => p + 1)}>
  Next
</button>

The Previous button disables on the first page. A real app also disables Next on the last page, which needs the server to report the total page count. The loading and error states around this list follow the same pattern as how to handle loading, error, empty, and success states.

Build infinite scroll with useInfiniteQuery

Infinite scroll uses useInfiniteQuery, which appends each page to the previous ones. The hook needs an initial page param and a function that returns the next param or undefined.

App.jsxApp.jsx
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  useInfiniteQuery({
    queryKey: ["posts"],
    queryFn: ({ pageParam }) =>
      fetch(`/api/posts?cursor=${pageParam}`).then((res) => res.json()),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

getNextPageParam reads the cursor from the last page. When it returns undefined, hasNextPage becomes false and the loader knows to stop. data.pages holds one array per fetched page.

App.jsxApp.jsx
{data.pages.map((page) =>
  page.items.map((post) => <li key={post.id}>{post.title}</li>)
)}
<button
  type="button"
  onClick={() => fetchNextPage()}
  disabled={!hasNextPage || isFetchingNextPage}
>
  {isFetchingNextPage ? "Loading more..." : "Load more"}
</button>
{isFetchingNextPage && <p role="status">Loading more posts...</p>}

The button calls fetchNextPage, which appends the next page to data.pages. The pages array grows with each fetch, so the list renders more items without replacing what is already shown. The role="status" message announces the fetch to screen reader users who cannot see the button text change.

The disabled prop stops the call when there is no next page or a fetch is already running. A scroll-triggered version can call the same function from an IntersectionObserver when a sentinel enters the viewport.

Which should you choose?

Choose pagination when users need to jump to a specific result, share a link, or work with large tables. Choose infinite scroll for feeds and timelines where scanning is the main action and interruption hurts. Start with pagination when in doubt, because it is easier to make accessible and to debug.

For a list that fits on one screen, neither pattern is needed; render it all at once. The two patterns can also coexist, with pagination on desktop and infinite scroll on mobile.

Pagination is also friendlier for accessibility, because buttons announce page changes and move focus predictably. Infinite scroll should offer a Load more button as a fallback for keyboard users, since scroll position alone is not enough. Both patterns run on the same client, set up in how to set up TanStack Query in React.

Rune AI

Rune AI

Key Insights

  • Pagination replaces pages; infinite scroll appends them.
  • Put the page number in the query key.
  • useInfiniteQuery needs initialPageParam and getNextPageParam.
  • Call fetchNextPage only when hasNextPage is true.
  • Offer a Load more button for keyboard users.
RunePowered by Rune AI

Frequently Asked Questions

When should I use pagination?

Use pagination for search results, tables, and anything where users need to jump to a specific page or share a link. It keeps memory flat and gives explicit control.

When should I use infinite scroll?

Use infinite scroll for feeds and timelines where scanning is the main action. Append pages with useInfiniteQuery and call fetchNextPage as the user reaches the end.

Is infinite scroll bad for accessibility?

It can be, because reaching the end requires scrolling. Provide a Load more button as a keyboard-accessible fallback, and announce loading with role status.

Conclusion

Pagination replaces one page with the next and is best for control and linking. Infinite scroll appends pages and is best for feeds. Build pagination with page state and infinite scroll with useInfiniteQuery.