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!
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 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:
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;
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 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
getNextPageParam
andgetPreviousPageParam
is available as an additional parameter in the query function, which can optionally be overridden when calling thefetchNextPage
orfetchPreviousPage
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 } })
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>
Hey there, want to help make our blog better?
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 nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
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.”