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.
| Aspect | Pagination | Infinite scroll |
|---|---|---|
| Data loaded | One page at a time | Pages accumulate |
| Navigation | Numbered or prev and next buttons | Scrolling or Load more |
| Memory | Constant, one page | Grows with pages loaded |
| Deep links | Easy with a page query | Harder, needs saved position |
| Best for | Search results and tables | Feeds 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.
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.
<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.
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.
{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
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.
Frequently Asked Questions
When should I use pagination?
When should I use infinite scroll?
Is infinite scroll bad for accessibility?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.