20
Implementing pagination with Next.js, MUI and react-query
If you need the data on your page fetched, cached, and beautifully paginated for an amazing user experience, you clicked on the right post. I implemented a solution to this problem a few days ago at work and wanted to share it with you:
I don't want to bore you with a long section about setup and creating boilerplate, so I will just assume you are familiar with the basics. You can also inspect the finished project in this respository if you are left with questions. Let's go:
- You will need a fresh Next.js project with react query and material-ui installed. I opted for material-ui v4 because that's what we have at work but feel free to use whatever version you want, just keep in mind that import statements and usage might differ slightly.
- The first thing you want to do is to get some data to be paginated from the Rick and Morty API. Instead of fetching inside a useEffect hook and then writing data into state, we are going to use react-query. To make it work, you will have to configure a provider in the _app.js file:
import "../styles/globals.css";
import { ReactQueryDevtools } from "react-query/devtools";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
function MyApp({ Component, pageProps }) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools>
</QueryClientProvider>
);
}
export default MyApp;
This is pure setup from the react-query docs: We configure a queryClient without options and wrap our application inside a QueryClientProvider. Besides, I added the ReactQueryDevtools to make it easier to see our data and how the cache works.
- Now, inside the index.js page or any other page of your choice, import the useQuery hook. It takes two arguments: the first one is a string that acts as a name for your query and the second one is the function you use for fetching data. Just to be able to see something on the page, I print the stringified data inside a div tag.
import { useQuery } from "react-query";
export default function PaginationPage(props) {
const { data } = useQuery(
"characters",
async () =>
await fetch(`https://rickandmortyapi.com/api/character/`).then((result) =>
result.json()
)
);
console.log(data);
return <div>{JSON.stringify(data)}</div>;
}
The result should look similar to the picture above. Keep in mind that you are still asynchronously fetching data, so as you can see in the console, there will be a moment at the beginning where the data object will be undefined. Also, if you click on the flower in the left corner, you open the react-query developer tools. There, you can see the query that was just executed and when you click on it, it even let's you see the fetched query data, so you don't actually need the console.log that I wrote.
- Now that we have some data inside our app, let's quickly set up something that looks decent to show the Rick and Morty Characters we just fetched:
<h1>Rick and Morty with React Query and Pagination</h1>
<div className='grid-container'>
{data?.results?.map((character) => (
<article key={character.id}>
<img
src={character.image}
alt={character.name}
height={200}
loading='lazy'
width={200}
/>
<div className='text'>
<p>Name: {character.name}</p>
<p>Lives in: {character.location.name}</p>
<p>Species: {character.species}</p>
<i>Id: {character.id} </i>
</div>
</article>
))}
</div>
Nothing fancy here: we iterate over the data if there is some and display an image and some data about the Character.
Here are the styles, I just wrote them in the globals.css file. It doesn't look super cool but it does the job.
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
max-width: 1300px;
width: 100%;
margin: auto;
padding: 2rem;
}
article {
padding: 1em;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
border-radius: 0.5em;
box-shadow: rgba(99, 99, 99, 0.5) 0px 2px 8px 0px;
}
Until now, our application cannot show data that is beyond the first 20 items the API returns by default, so let's change that.
- Import the Material UI pagination component and put it above the grid container. The count prop controls how many pages will be displayed and we already got this information from the API.
import Pagination from "@material-ui/lab/Pagination";
...
return (
<div>
<h1>Rick and Morty with React Query and Pagination</h1>
<Pagination
count={data?.info.pages}
variant='outlined'
color='primary'
className='pagination'
/>
<div className='grid-container'>
...
- Then, set up some state to save the page we are currently on and add the page parameter to the API call. This also implies that we can give the current page to our MUI pagination component, so it knows which number to highlight.
import { useState } from "react";
...
const [page, setPage] = useState(1);
const { data } = useQuery(
"characters",
async () =>
await fetch(
`https://rickandmortyapi.com/api/character/?page=${page}`
).then((result) => result.json())
);
return (
<div>
<h1>Rick and Morty with React Query and Pagination</h1>
<Pagination
count={data?.info.pages}
variant='outlined'
color='primary'
className='pagination'
page={page}
/>
...
- As the last step, we will need to define the onChange handler for the Pagination component. The handler updates the page state and also does a shallow push to the url. To make react-query fetch new data, we must add the page variable to the query key. Instead of the string "characters", we will pass in an array that contains the string and all the variables that we want to trigger a new API call.
import { useRouter } from "next/router";
...
const router = useRouter();
const { data } = useQuery(
["characters", page],
async () =>
await fetch(
`https://rickandmortyapi.com/api/character/?page=${page}`
).then((result) => result.json())
);
function handlePaginationChange(e, value) {
setPage(value);
router.push(`pagination/?page=${value}`, undefined, { shallow: true });
}
return (
<div>
<h1>Rick and Morty with React Query and Pagination</h1>
<Pagination
count={data?.info.pages}
variant='outlined'
color='primary'
className='pagination'
page={page}
onChange={handlePaginationChange}
/>
Now, pagination already works like a charm! Click yourself through the different pages and get all confused by all the characters you didn't know although you did see all the seasons of Rick and Morty....
Two tiny things are not working properly here: The first one is that when a user visits the URL my-domain.com/pagination?page=5
directly, our application will not show the results from page 5, since we are never reading the query parameters on page load. We can solve this with a useEffect hook that reads the queryParam from the Next.js router object than only runs when everything is mounted for the first time:
useEffect(() => {
if (router.query.page) {
setPage(parseInt(router.query.page));
}
}, [router.query.page]);
On the other hand, when you click from one page to the next, you will see the Pagination component flicker: With every fetch, it is getting information on how long it should be, but while the fetching occurs, since data is undefined, it shrinks to show only one page. We can avoid that by setting a configuration object on our useQuery hook this way:
const { data } = useQuery(
["characters", page],
async () =>
await fetch(
`https://rickandmortyapi.com/api/character/?page=${page}`
).then((result) => result.json()),
{
keepPreviousData: true,
}
);
The keepPreviousData instruction will keep the previous data in the data object while the fetching occurs and replace it when it already has new data, therefore avoiding the situation where data is left undefined for a moment.
I hope this helped! Let me know if you could make it work or if you have some feedback.
Now, if you will excuse me, I have to view some Rick and Morty now because all these characters made me really want to rewatch the show.
20