Pagination and Infinite Scroll with React Query V3

Master the art of loading large datasets efficiently with React Query's powerful pagination primitives. Build seamless infinite scroll experiences that delight users.

Understanding Infinite Queries in React Query

React Query introduces the concept of "infinite queries" to handle scenarios where data arrives in sequential pages or chunks that can be incrementally loaded. Unlike standard queries that fetch a single dataset, infinite queries maintain a running collection of all fetched pages, allowing applications to display progressively loaded content seamlessly.

For a deeper dive into infinite scroll patterns, explore our guide on implementing infinite scroll with React hooks to understand advanced techniques for creating seamless user experiences.

The useInfiniteQuery hook builds upon the foundation of useQuery but extends it with pagination-aware return values and functions for fetching additional pages on demand. This approach handles caching, deduplication, and background updates automatically, freeing developers to focus on user experience rather than data management complexity.

The key distinction between useQuery and useInfiniteQuery lies in how they structure and return data. While useQuery returns a simple data object, useInfiniteQuery organizes data into pages with each page containing its own set of items. This structure mirrors how backend APIs typically paginate large datasets, whether through page numbers or cursor-based pagination where each response includes a reference to the next batch of data.

The useInfiniteQuery Hook Structure

The useInfiniteQuery hook accepts a query key and a query function, similar to useQuery, but extends the options object with pagination-specific configurations. The most critical of these is the getNextPageParam function, which tells React Query how to determine whether more data exists and what parameters to use when fetching the next page. This function receives the last page of fetched data as its first argument and should return either a cursor value for the next page or undefined if no more data exists.

The initialPageParam configuration specifies the starting cursor value for the first page request. This is essential when your API expects a specific starting point, such as a timestamp, ID, or offset value. For most cursor-based implementations, this defaults to null or zero, but explicit configuration ensures predictable behavior across different data sources.

The query function for infinite queries must return data in a structure that includes both the items for the current page and the cursor information needed for subsequent requests. This typically involves returning an object with an array of items alongside a nextCursor property that indicates where the next page begins. For cursor-based pagination, this cursor often represents an ID or timestamp from the last item fetched, allowing the backend to efficiently retrieve subsequent records using indexed lookups.

useInfiniteQuery Hook Signature
1const {2 data,3 fetchNextPage,4 hasNextPage,5 isFetchingNextPage,6 status,7} = useInfiniteQuery({8 queryKey: ['posts'],9 queryFn: fetchPosts,10 getNextPageParam: (lastPage) => lastPage.nextCursor,11 initialPageParam: 0,12});

Key Return Values and Their Uses

The useInfiniteQuery hook provides several return values specifically designed for infinite scrolling interfaces. The data.pages property contains an array of all fetched pages, where each page object includes the items returned for that specific request along with metadata about that page's position in the sequence. The data.pageParams array maintains the cursor values used for each page request, enabling precise cache reconstruction and debugging of the pagination state.

The fetchNextPage function triggers loading of subsequent pages and should be called when users scroll near the end of displayed content or when they click a "load more" button. This function returns a promise that resolves when the page fetch completes, allowing components to coordinate loading states and error handling. Call this function proactively based on scroll position using intersection observers or reactively based on user interaction with a dedicated button.

The hasNextPage boolean indicates whether more data is available to load, derived from the return value of getNextPageParam on the most recently fetched page. This value is crucial for implementing "load more" buttons or infinite scroll triggers, as it prevents unnecessary API calls when all data has been fetched. When hasNextPage is false, components should hide or disable loading triggers and potentially display an "end of content" indicator to provide clear feedback to users.

tRPC Integration with useInfiniteQuery

tRPC provides a type-safe integration with React Query's useInfiniteQuery, eliminating the need to manually define query keys and providing compile-time type safety for query parameters and responses. When defining a tRPC procedure for infinite queries, the input schema must include a cursor field of any type, which tRPC uses to automatically construct the useInfiniteQuery hook for the frontend. This tight integration ensures that any changes to the backend schema are immediately reflected in frontend types.

This full-stack type safety pattern is a hallmark of modern web development practices where frontend and backend share types seamlessly throughout the application.

The tRPC approach to infinite queries follows a consistent pattern: the procedure accepts a cursor along with optional limit and filtering parameters, fetches the requested number of items plus one extra (to determine if more data exists), and returns both the items and the next cursor. This "fetch n+1 items" pattern is a common backend technique for cursor-based pagination that efficiently determines whether additional pages exist without counting total records.

tRPC Backend Procedure
1const appRouter = t.router({2 infinitePosts: t.procedure3 .input(z.object({4 limit: z.number().min(1).max(100).nullish(),5 cursor: z.number().nullish(),6 }))7 .query(async ({ input }) => {8 const { cursor, limit = 50 } = input;9 const items = await prisma.post.findMany({10 take: limit + 1,11 cursor: cursor ? { myCursor: cursor } : undefined,12 orderBy: { myCursor: 'asc' },13 });14 let nextCursor: typeof cursor | undefined = undefined;15 if (items.length > limit) {16 const nextItem = items.pop();17 nextCursor = nextItem!.myCursor;18 }19 return { items, nextCursor };20 }),21});
tRPC Frontend Component
1function PostsList() {2 const myQuery = trpc.infinitePosts.useInfiniteQuery(3 { limit: 10 },4 { getNextPageParam: (lastPage) => lastPage.nextCursor }5 );6 7 const allPosts = myQuery.data?.pages.flatMap(p => p.items) ?? [];8 9 return (10 <div>11 {allPosts.map((post) => <PostCard key={post.id} post={post} />)}12 {myQuery.hasNextPage && (13 <button onClick={() => myQuery.fetchNextPage()}>14 {myQuery.isFetchingNextPage ? 'Loading...' : 'Load More'}15 </button>16 )}17 </div>18 );19}

Best Practices for Production Implementations

Production infinite scroll implementations require attention to several reliability concerns that may not be apparent in simple examples. Error handling must account for both initial load failures and pagination-specific errors, providing appropriate feedback without losing already-loaded content.

Error Handling Strategies

  • Distinguish between initial load errors and pagination errors
  • Initial load failures should display full retry UI with prominent retry controls
  • Pagination errors should preserve already-loaded content while offering page-specific retry
  • Use the isError property from useInfiniteQuery to detect failures, and distinguish between initial and subsequent page errors

Cache Management

  • Monitor accumulated cached data growth as users load more pages
  • React Query automatically garbage collects unused query data, but applications with strict memory requirements should implement strategies for limiting cached pages
  • The keepPreviousData option can smooth transitions during cache invalidation

Performance Optimization

  • Consider virtualization for large lists using libraries like react-window or tanstack/virtual
  • These libraries render only the visible portion of loaded content, dramatically reducing DOM node counts for lists with hundreds or thousands of items
  • Implement prefetching for predicted scroll patterns using fetchNextPage with the prefetch option
  • Optimize images and rendered content per item using lazy loading techniques

For foundational concepts of infinite scroll implementation, see our guide on infinite scroll fundamentals to understand the core patterns before diving into React Query optimizations.

Optimistic Updates and Cache Manipulation

When mutations affect data within infinite queries, React Query provides helper utilities for updating cached data without requiring a refetch. The getInfiniteData helper retrieves the current cached state of an infinite query, allowing components to read the complete paginated dataset including all loaded pages.

The setInfiniteData helper writes updated state back to the cache, enabling components to modify items within pages or add new items to existing pages. This function accepts the query key and an updater function that receives the current data and returns the modified version. The updater function receives the entire pages array, enabling precise modifications to specific items within any page.

The onMutate pattern in mutations provides a powerful approach for optimistic updates. By canceling outgoing refetches, snapshotting previous data, and setting new optimistic data before the mutation completes, applications can provide instant user feedback while maintaining cache consistency. This pattern is essential for maintaining UI consistency when users create, update, or delete items within a paginated list.

Cache Manipulation Example
1trpc.posts.delete.useMutation({2 async onMutate(opts) {3 await utils.posts.infinitePosts.cancel();4 utils.posts.infinitePosts.setInfiniteData(5 { limit: 10 },6 (data) => {7 if (!data) return { pages: [], pageParams: [] };8 return {9 ...data,10 pages: data.pages.map((page) => ({11 ...page,12 items: page.items.filter(i => i.id !== opts.id),13 })),14 };15 }16 );17 },18});

Common Patterns and Use Cases

Infinite scroll excels in scenarios where continuous content discovery enhances user experience. Social media feeds, e-commerce product listings, search results, and activity logs all benefit from the seamless loading pattern that infinite queries provide.

When Infinite Scroll Works Best

  • Social media feeds - Continuous browsing of posts and updates where users scroll through content sequentially
  • E-commerce listings - Product discovery without page breaks maintains shopping momentum
  • Search results - When users want to browse rather than navigate to specific positions
  • Activity logs - Timeline-based content consumption where sequential viewing is natural

When to Use Numbered Pagination Instead

  • Content requiring specific item reference or citation where users might need to share specific page URLs
  • Searchable content where users need specific positions in the result set
  • Large datasets with clear categorization boundaries where jumping to sections makes sense
  • When total dataset size should be visible to set user expectations

Error Recovery and Boundary Handling

  • Network errors during initial load should display full retry UI without preventing future page loads
  • Errors during pagination should preserve already-loaded content while offering a retry mechanism specifically for the failed page
  • API rate limiting or maximum page size restrictions may require adjusting page sizes dynamically
  • React Query's built-in retry mechanisms automatically handle transient network failures

For more on building performant user interfaces, explore our React development services and learn how we create seamless user experiences across web applications.

Build Better Data Loading Experiences

Key capabilities when implementing pagination and infinite scroll

Type-Safe Integration

Combine React Query with tRPC for end-to-end type safety across your entire pagination implementation.

Smart Caching

React Query handles deduplication, background updates, and cache management automatically.

Optimistic Updates

Update the UI instantly when users interact with paginated content, then sync with the server.

Performance at Scale

Virtualization and prefetching strategies for handling thousands of items smoothly.

Conclusion

Implementing pagination and infinite scroll with React Query V3 provides a robust foundation for building performant, user-friendly data loading experiences. Whether using traditional page-based pagination with keepPreviousData or embracing infinite scroll through useInfiniteQuery, the library handles the complex state management, caching, and deduplication concerns that would otherwise require significant custom implementation.

When combined with tRPC's type-safe integration, developers gain additional confidence in their pagination implementations through compile-time type checking. The patterns covered in this guide--from hook configuration to cache manipulation, from tRPC integration to performance optimization--represent the essential toolkit for production-ready infinite query implementations.

By understanding both the technical mechanics and the user experience implications of different pagination approaches, developers can make informed decisions about which patterns best serve their application's specific needs. Our team specializes in building seamless data loading experiences that delight users--contact us to discuss how we can help with your next project.

  1. TanStack Query v5 - Infinite Queries Documentation
  2. LogRocket - Pagination and Infinite Scroll with React Query
  3. tRPC - useInfiniteQuery Documentation

Frequently Asked Questions

What is the difference between useQuery and useInfiniteQuery?

useQuery fetches a single dataset and returns it directly, while useInfiniteQuery manages paginated data with a pages structure, providing fetchNextPage and hasNextPage for pagination control.

How do I handle errors in infinite scroll?

Use the isError and error properties to detect failures. Distinguish between initial load errors (show full retry) and pagination errors (preserve loaded content, offer targeted retry).

What is getNextPageParam?

getNextPageParam is a function you provide that receives the last fetched page and returns the cursor for the next page, or undefined if no more data exists.

How does tRPC integrate with useInfiniteQuery?

tRPC automatically generates useInfiniteQuery hooks from procedures with cursor input. The backend returns items and nextCursor, which tRPC maps to the infinite query structure.

Should I use infinite scroll or numbered pagination?

Use infinite scroll for discovery-oriented content like feeds and product listings. Use numbered pagination when users need to reference specific items or navigate to exact positions.

How can I optimize infinite scroll performance?

Use virtualization libraries like react-window, implement prefetching, and optimize item rendering with lazy loading images and memoized components.

Ready to Build Seamless Data Experiences?

Our team of React specialists can help you implement performant pagination and infinite scroll patterns that delight users.

Sources

  1. TanStack Query v5 - Infinite Queries Documentation - Core reference for infinite query implementation, cache behavior, and pagination patterns
  2. LogRocket - Pagination and Infinite Scroll with React Query - Practical implementation examples with code samples
  3. tRPC - useInfiniteQuery Documentation - tRPC integration patterns with React Query infinite queries