DEV Community

Cover image for Pagination and infinite scroll with React Query v3
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Pagination and infinite scroll with React Query v3

Written by Chiamaka Umeh✏️

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:

React Query Random User API

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

npx create-react-app app-name
npm install react-query
Enter fullscreen mode Exit fullscreen mode

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:

//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')
);
Enter fullscreen mode Exit fullscreen mode

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(), 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;
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
)
Enter fullscreen mode Exit fullscreen mode

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'>
      &lt;img src={user.picture.large} />
      <h3>{user.name.first}{user.name.last}</h3>
    </div>
  );
}

export default Users;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

//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
        }
    })
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

LogRocket signup

LogRocket 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.

Top comments (0)