Pagination and infinite scroll with React Query v3
Tags: react
Published: 2022-04-22 17:20:24

**Written by [Chiamaka Umeh](https://blog.logrocket.com/author/chiamakaumeh/)✏️**

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](https://randomuser.me/documentation), 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:

![React Query Random User API](https://blog.logrocket.com/wp-content/uploads/2022/04/react-query-random-user-api.gif)

Let's get started!

*   React Query
*   Set up the project
    *   Setting up React Query
    *   Pagination with `useQuery` and `keepPreviousData`
*   Infinite Scroll with `useInfiniteQuery`
*   Conclusion

## React Query

[React Query makes it easy to fetch, cache, sync, and update server state](https://blog.logrocket.com/whats-new-in-react-query-3/) 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.

## Set up the project

We'll start by initializing a new React app and installing React Query as follows:

```bash
npx create-react-app app-name
npm install react-query
```

Start the server with `npm start`, and let's dive in!

### Setting up React Query

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:

```js
//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')
);
```

### Pagination with `useQuery` and `keepPreviousData`

The `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:

*   The previous data from the last successful fetch will be available even though the query key has changed
*   As soon as the new data arrives, the previous data will be swapped with the new data
*   `isPreviousData` checks what data the query is currently providing

In previous versions of React Query, pagination was achieved with [`usePaginatedQuery()`](https://react-query.tanstack.com/guides/migrating-to-react-query-3), which has been deprecated at the time of writing. Let's create a new component in the `src` folder and call it `Pagination.js`:

```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:

```js
// 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 fetching
*   `isError or status === 'error'`: The query encountered an error
*   `isSuccess or status === 'success'`: The query was successful and data is available

We also have `isPreviousData`, which was made available because we set `keepPreviousData` to `true`. Using this information, we can display the result inside a JSX:

```js
// 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`:

```js
//Users.js

import React from 'react';

const Users = ({ user }) => {
  return (
    <div className='card-detail'>
      &lt;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:

```js
  // 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:

```js
// 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;
```

## Infinite Scroll with `useInfiniteQuery`

Instead 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 data
*   `data.pages` is an array containing the fetched pages
*   `data.pageParams` is an array containing the page params used to fetch the pages
*   The `fetchNextPage` and `fetchPreviousPage` functions are now available
*   `getNextPageParam` and `getPreviousPageParam` options are both available for determining if there is more data to load and the information to fetch it
*   A `hasNextPage`, which is `true` if `getNextPageParam` returns a value other than undefined
*   A `hasPreviousPage`, which is `true` if `getPreviousPageParam` returns a value other than undefined
*   The `isFetchingNextPage` and `isFetchingPreviousPage` booleans distinguish between a background refresh state and a loading more state

> Note: The information supplied by `getNextPageParam` and `getPreviousPageParam` is available as an additional parameter in the query function, which can optionally be overridden when calling the `fetchNextPage` or `fetchPreviousPage` functions.

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:

```js
//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:

```js
// 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:

```js
// 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:

```js
// 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:

```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;
}
```

## Conclusion

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!

---

## Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket](https://www2.logrocket.com/react-performance-monitoring). 

![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)

[LogRocket](https://www2.logrocket.com/react-performance-monitoring) is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — [start monitoring for free](https://www2.logrocket.com/react-performance-monitoring).