DEV Community

Cover image for Implementing pagination with Next.js, MUI and react-query
Elisabeth Leonhardt
Elisabeth Leonhardt

Posted on • Updated on

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:

1. Setting up the project

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

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.

2. Fetch and display data with react-query

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

React query set up correctly

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

Nothing fancy here: we iterate over the data if there is some and display an image and some data about the Character.
Character Cards without pagination
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;
}
Enter fullscreen mode Exit fullscreen mode

Until now, our application cannot show data that is beyond the first 20 items the API returns by default, so let's change that.

3. Adding Pagination with Material UI

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

Pagination component

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

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

4. Cosmetic improvements

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

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

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.

Discussion (2)

Collapse
tripol profile image
Ekanem

Very informative. Thanks for this. On a personal preference, what do you use normally use to handle HTTP requests?

1) Fetch API
2) Axios
3) React Query
4) Others

Collapse
elisabethleonhardt profile image
Elisabeth Leonhardt Author

Glad that you liked it!
first of all: keep in mind that React Query is not an alternative to the fetch API or Axios: if you take a look at the useQuery hook, I pass in a function that fetches code with the fetch API:

const { data } = useQuery(
    ["characters", page],
    async () =>
      await fetch(
        `https://rickandmortyapi.com/api/character/?page=${page}`
      ).then((result) => result.json()),
    {
      keepPreviousData: true,
    }
  );

Enter fullscreen mode Exit fullscreen mode

Without useQuery, you would use fetch/axios and then write your data into state, besides managing loading state and error state yourself. React Query does all of this for you, which has simplified my code a lot since I started using it. You can read more about why to use React Query in this article if you are interested.

When I have to decide between fetch and axios, I tend to decide like this:

  • if the project I work on already uses fetch or axios, I just use whatever they are already using to be consistent.
  • in my personal projects, I prefer fetch because it doesn't require me to install an additional library.