23
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:
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.
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.
<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>
.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 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'>
...

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}
/>
...
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.
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.
23