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
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
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') );
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;
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 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> )
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;
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 datadata.pages
is an array containing the fetched pagesdata.pageParams
is an array containing the page params used to fetch the pages- The
fetchNextPage
andfetchPreviousPage
functions are now available getNextPageParam
andgetPreviousPageParam
options are both available for determining if there is more data to load and the information to fetch it- A
hasNextPage
, which istrue
ifgetNextPageParam
returns a value other than undefined - A
hasPreviousPage
, which istrue
ifgetPreviousPageParam
returns a value other than undefined - The
isFetchingNextPage
andisFetchingPreviousPage
booleans distinguish between a background refresh state and a loading more state
Note: 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; }
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!
The post Pagination and infinite scroll with React Query v3 appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/c9letDI
via Read more