 
        
         
        When large data sets are handled incorrectly, both developers and end users feel the negative effects. Two popular UI patterns that frontend developers can use to efficiently render large data sets are pagination and infinite scroll. These patterns improve an application’s performance by only rendering or fetching small chunks of data at a time, greatly improving UX by allowing users to easily navigate through the data.
 
In this tutorial, we’ll learn how to implement pagination and infinite scroll using React Query. We’ll use the Random User API, which allows you to fetch up to 5,000 random users either in one request or in small chunks with pagination. This article assumes that you have a basic understanding of React. The gif below is a demo of what we’ll build:

Let’s get started!
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
React Query makes it easy to fetch, cache, sync, and update server state in React applications. React Query offers features like data caching, deduplicating multiple requests for the same data into a single request, updating state data in the background, performance optimizations like pagination and lazy loading data, memoizing query results, prefetching the data, mutations, and more, which allow for seamless management of server-side state.
All of these functionalities are implemented with just a few lines of code, and React Query handles the rest in the background for you.
We’ll start by initializing a new React app and installing React Query as follows:
npx create-react-app app-name npm install react-query
Start the server with npm start, and let’s dive in!
To initialize a new instance of React Query, we’ll import QueryClient and QueryClientProvider from React Query. Then, we wrap the app with QueryClientProvider as shown below:
//App.js
import {
  QueryClient,
  QueryClientProvider,
} from 'react-query'
const queryClient = new QueryClient()
ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root')
);
useQuery and keepPreviousDataThe useQuery Hook is used to fetch data from an API. A query is a declarative dependency on an asynchronous source of data that has a unique key. To implement pagination, we ideally need to increment or decrement the pageIndex, or cursor, for a query. Setting the keepPreviousData to true will give us the following benefits:
isPreviousData checks what data the query is currently providingIn previous versions of React Query, pagination was achieved with usePaginatedQuery(), which has been deprecated at the time of writing. Let’s create a new component in the src folder and call it Pagination.js:
// Pagination.js
import React from 'react'
function Pagination() {
  return (
    <div>Pagination View</div>
  )
}
export default Pagination;
Next, we’ll write a function that will fetch the data and pass it to the useQuery Hook:
// Pagination.js
const [page, setPage] = useState(1);
const fetchPlanets = async (page) => {
  const res = await fetch(`https://randomuser.me/api/page=${page}&results=10&seed=03de891ee8139363`);
  return res.json();
}
const {
    isLoading,
    isError,
    error,
    data,
    isFetching,
    isPreviousData
  } = useQuery(['users', page], () => fetchPlanets(page), { keepPreviousData: true });
Notice how we are passing in a page number and results=10, which will fetch only ten results per page.
The useQuery Hook returns the data as well as important states that can be used to track the request at any time. A query can only be in one of the these states at any given moment.
isLoading or status === 'loading': The query has no data and is currently fetchingisError or status === 'error': The query encountered an errorisSuccess or status === 'success': The query was successful and data is availableWe also have isPreviousData, which was made available because we set keepPreviousData to true. Using this information, we can display the result inside a JSX:
// Pagination.js
if (isLoading) {
    return <h2>Loading...</h2>
  }
  if (isError) {
    return <h2>{error.message}</h2>
  }
return (
 <div>
      <h2>Paginated View</h2>
      {data && (
        <div className="card">
          {data?.results?.map(user => <Users key={user.id} user={user} />)}
        </div>
      )}
      <div>{isFetching ? 'Fetching...' : null}</div>
    </div>
)
To display the fetched data, we’ll create a reusable stateless component called Users:
//Users.js
import React from 'react';
const Users = ({ user }) => {
  return (
    <div className='card-detail'>
      <img src={user.picture.large} />
      <h3>{user.name.first}{user.name.last}</h3>
    </div>
  );
}
export default Users;
Next, in the Pagination.js file, we’ll implement navigation for users to navigate between different pages:
  // Pagination.js
   <div className='nav btn-container'>
        <button
          onClick={() => setPage(prevState => Math.max(prevState - 1, 0))}
          disabled={page === 1}
        >Prev Page</button>
        <button
          onClick={() => setPage(prevState => prevState + 1)}
        >Next Page</button>
      </div>
In the code below, we increment or decrement the page number to be passed to the APIs according to what button the user clicks:
// Pagination.js
import React, { useState } from 'react';
import { useQuery } from 'react-query';
import User from './User';
const fetchUsers = async (page) => {
  const res = await fetch(`https://randomuser.me/api/?page=${page}&results=10&seed=03de891ee8139363`);
  return res.json();
}
const Pagination = () => {
  const [page, setPage] = useState(1);
  const {
    isLoading,
    isError,
    error,
    data,
    isFetching,
  } = useQuery(['users', page], () => fetchUsers(page), { keepPreviousData: true });
  if (isLoading) {
    return <h2>Loading...</h2>
  }
  if (isError) {
    return <h2>{error.message}</h2>
  }
  return (
    <div>
      <h2>Paginated View</h2>
      {data && (
        <div className="card">
          {data?.results?.map(user => <User key={user.id} user={user} />)}
        </div>
      )}
      <div className='nav btn-container'>
        <button
          onClick={() => setPage(prevState => Math.max(prevState - 1, 0))}
          disabled={page === 1}
        >Prev Page</button>
        <button
          onClick={() => setPage(prevState => prevState + 1)}
        >Next Page</button>
      </div>
      <div>{isFetching ? 'Fetching...' : null}</div>
    </div>
  );
}
export default Pagination;
useInfiniteQueryInstead of the useQuery Hook, we’ll use the useInfiniteQuery Hook to load more data onto an existing set of data.
There are a few things to note about useInfiniteQuery:
data is now an object containing infinite query datadata.pages is an array containing the fetched pagesdata.pageParams is an array containing the page params used to fetch the pagesfetchNextPage and fetchPreviousPage functions are now availablegetNextPageParam and getPreviousPageParam options are both available for determining if there is more data to load and the information to fetch ithasNextPage, which is true if getNextPageParam returns a value other than undefinedhasPreviousPage, which is true if getPreviousPageParam returns a value other than undefinedisFetchingNextPage and isFetchingPreviousPage booleans distinguish between a background refresh state and a loading more stateNote: The information supplied by
getNextPageParamandgetPreviousPageParamis available as an additional parameter in the query function, which can optionally be overridden when calling thefetchNextPageorfetchPreviousPagefunctions.
Let’s create another component in the src folder called InfiniteScroll.js. We’ll write the function for fetching data and pass that to the useInfiniteQuery Hook as below:
//InfiniteScroll.js
const fetchUsers = async ({ pageParam = 1 }) => {
    const res = await fetch(`https://randomuser.me/api/?page=${pageParam}&results=10`);
    return res.json();
}
    const {
        isLoading,
        isError,
        error,
        data,
        fetchNextPage,
        isFetching,
        isFetchingNextPage
    } = useInfiniteQuery(['colors'], fetchUsers, {
        getNextPageParam: (lastPage, pages) => {
            return lastPage.info.page + 1
        }
    })
With the code above, we can easily implement a load more button on our UI by waiting for the first batch of data to be fetched, returning the information for the next query in the getNextPageParam, then calling the fetchNextPage to fetch the next batch of data.
Let’s render the data retrieved and implement a load more button:
// InfiniteScroll.js
if (isLoading) {
        return <h2>Loading...</h2>
    }
    if (isError) {
        return <h2>{error.message}</h2>
    }
    return (
        <>
            <h2>Infinite Scroll View</h2>
            <div className="card">
                {data.pages.map(page =>
                    page.results.map(user => <User key={user.id} user={user} />)
                )}
            </div>
            <div className='btn-container'>
                <button onClick={fetchNextPage}>Load More</button>
            </div>
            <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
        </>
    )
To display data, we reuse the Users component.
Notice how we are calling the fetchNextPage when the load more button is clicked. The value returned in the getNextPageParam is automatically passed to the endpoint in order to fetch another set of data:
// InfiniteScroll.js
import { useInfiniteQuery } from 'react-query'
import User from './User';
const fetchUsers = async ({ pageParam = 1 }) => {
    const res = await fetch(`https://randomuser.me/api/?page=${pageParam}&results=10`);
    return res.json();
}
const InfiniteScroll = () => {
    const {
        isLoading,
        isError,
        error,
        data,
        fetchNextPage,
        isFetching,
        isFetchingNextPage
    } = useInfiniteQuery(['colors'], fetchUsers, {
        getNextPageParam: (lastPage, pages) => {
            return lastPage.info.page + 1
        }
    })
    if (isLoading) {
        return <h2>Loading...</h2>
    }
    if (isError) {
        return <h2>{error.message}</h2>
    }
    return (
        <>
            <h2>Infinite Scroll View</h2>
            <div className="card">
                {data.pages.map(page =>
                    page.results.map(user => <User key={user.id} user={user} />)
                )}
            </div>
            <div className='btn-container'>
                <button onClick={fetchNextPage}>Load More</button>
            </div>
            <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
        </>
    )
}
export default InfiniteScroll;
Let’s import the components in the App.js and render them appropriately:
// App.js
import './App.css';
import Pagination from './Pagination';
import InfiniteScroll from './InfiniteScroll';
import { useState } from 'react';
function App() {
  const [view, setView] = useState('pagination')
  return (
    <div >
      <h1>Welcome to Random Users</h1>
      <nav className='nav'>
        <button onClick={() => setView('pagination')}>Pagination</button>
        <button onClick={() => setView('infiniteScroll')}>Infinite Scroll</button>
      </nav>
      {view === 'pagination' ? <Pagination /> : <InfiniteScroll />}
    </div>
  );
}
export default App;
Finally, we add the CSS:
body {
  margin: 0;
  font-family: sans-serif;
  background: #222;
  color: #ddd;
  text-align: center;
}
.card{
  display: flex;
  justify-content: space-between;
  text-align: center;
  flex-wrap: wrap;
  flex: 1;
}
.card-detail{
  box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
  width: 15rem;
  height: 15rem;
  margin: 1rem;
}
.card-detail h3{
  color: #ffff57;
}
.btn-container{
  text-align: center;
  margin-bottom: 5rem;
  margin-top: 2rem;
}
.nav{
  text-align: center;
}
.nav button{
  margin-right: 2rem;
}
button{
  padding: 0.5rem;
  background-color: aqua;
  border: none;
  border-radius: 10px;
  cursor: pointer;
}
In this article, we learned how to implement pagination and infinite scroll using React Query, a very popular React library for state management. React Query is often described as the missing piece in the React ecosystem. We’ve seen in this article how we can fully manage the entire request-response cycle with no ambiguity by just calling a Hook and passing in a function.
I hope you enjoyed this article! Be sure to leave a comment if you have any questions. Happy coding!
            Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
            server-side
            
$ npm i --save logrocket 
// Code:
import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
                    
                    
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
                    
                    
line-clamp to trim lines of textMaster the CSS line-clamp property. Learn how to truncate text lines, ensure cross-browser compatibility, and avoid hidden UX pitfalls when designing modern web layouts.

Discover seven custom React Hooks that will simplify your web development process and make you a faster, better, more efficient developer.

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now 
         
         
        
6 Replies to "Pagination and infinite scroll with React Query v3"
Congratulations! Can you give the link of github code?
Thanks. This is the codesandbox link : https://codesandbox.io/s/pagination-and-infinite-scroll-with-react-query-ib4uel?file=/src/index.js
Thank you! i’d also like a like to the have github/sandbox code please.
Welcome! 🙏 . This is the codesandbox link https://codesandbox.io/s/pagination-and-infinite-scroll-with-react-query-ib4uel?file=/src/index.js
Hello, well i would like to point a potential problem for this code.
“`js
const {
isLoading,
isError,
error,
data,
fetchNextPage,
isFetching,
isFetchingNextPage
} = useInfiniteQuery([‘colors’], fetchUsers, {
getNextPageParam: (lastPage, pages) => {
return lastPage.info.page + 1
}
})
“`
When you get the lastPage.info.page + 1 the react-query will never understand when the pages has come to an end, so to fix this you need to put a nextPage that says if theres a next page like has been done on react-query documentation https://github.com/TanStack/query/blob/main/examples/load-more-infinite-scroll/pages/index.js
Hi,
FYI – looking at your code you’re calling the fetchNextPage like this:
Load More
However based on the documentation, this should be avoided instead the callback should be passed like this:
fetchNextPage()}>Load More
You can read about it here: https://tanstack.com/query/v4/docs/guides/infinite-queries?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/infinite-queries
Quote from the docs:
“Note: It’s very important you do not call fetchNextPage with arguments unless you want them to override the pageParam data returned from the getNextPageParam function. e.g. Do not do this: as this would send the onClick event to the fetchNextPage function.”