Nader's Daily Blog
Welcome to Every Developers favorite blog in the Devosphere

New product features | The latest in technology | The weekly debugging nightmares & more!

Create server side Pagination in Next js 13

Create server side Pagination in Next js 13

July 24, 2023

Nader Elmahdy

Nader Elmahdy

Take my knowledge and shove it up your brain...

You know what's better than pagination? Server side Pagination.

Next js

If you're dealing with large sums of data, it's essential to limit how much data is being rendered on the screen.

You can achieve this by using one of two methods:

  1. Infinite scroll, where new data gets rendered when the user is about the reach the bottom.
  2. Pagination, where the user clicks on the number of the page he wants to navigate to which we're going to discuss.

It doesn't matter how you fetch your data, whether you're using Prisma, groq, raw MongoDB queries, or something else, there's always one way or another to limit the number of results being returned from the query.


In this example, I'm going to use Prisma, but the same principles apply to any other method.

We're going to start with a simple code that fetches some data from the database and displays it on the page

page.tsx

1import { prisma } from "@/lib/prisma";
2import ProductCard from "../components/ProductCard";
3async function page() {
4  const data = await prisma.product.findMany();
5  return (
6    <div>
7      <h1 className="text-2xl font-bold text-center my-10">
8        {data.length} Products
9      </h1>
10      <div className="flex flex-wrap gap-10 justify-center my-10">
11        {data.map((product) => (
12          <ProductCard key={product.id} product={product} />
13        ))}
14      </div>
15    </div>
16  );
17}
18
19export default page;
20

The above code just fetches everything in the database.

As you can see in our case the data isn't that big (only 202 products), but in real life scenario this could be thousands of products, and it would be a terrible experience for your users.


Take and Skip

In Prisma, we have 2 functions that allow us to implement pagination.

take specifies how many documents we want to return from our database.

skip specifies how many documents we want to skip, so for example, if I say take:20, skip:5, this will skip the first 5 results and return the following 20.


Now let's use that in our previous example.

Instead of returning all the data, I'm going to take only 10.

page.tsx

1import { prisma } from "@/lib/prisma";
2import ProductCard from "../components/ProductCard";
3async function page() {
4  const data = await prisma.product.findMany({
5    take: 10,
6  });
7  return (
8    <div>
9      <h1 className="text-2xl font-bold text-center my-10">
10        {data.length} Products
11      </h1>
12      <div className="flex flex-wrap gap-10 justify-center my-10">
13        {data.map((product) => (
14          <ProductCard key={product.id} product={product} />
15        ))}
16      </div>
17    </div>
18  );
19}
20
21export default page;
22

And that works perfectly fine, but what if I want to go to the second page?

The process should look something like this:

skip the first 10 products then give me the 10 products after.

In code, it looks like this:

page.tsx

1import { prisma } from "@/lib/prisma";
2import ProductCard from "../components/ProductCard";
3async function page() {
4  const data = await prisma.product.findMany({
5    skip: 10,
6    take: 10,
7  });
8  return (
9    <div>
10      <h1 className="text-2xl font-bold text-center my-10">
11        {data.length} Products
12      </h1>
13      <div className="flex flex-wrap gap-10 justify-center my-10">
14        {data.map((product) => (
15          <ProductCard key={product.id} product={product} />
16        ))}
17      </div>
18    </div>
19  );
20}
21
22export default page;
23

Ok, so now we have a basic understanding of how to implement pagination.


Next, we need to know which page we're currently on, so we need to save the number of the current page, remember that we need to keep this a server component, we don't want to use any hooks to store the page number, so for that, we're going to get the page number from a query from the URL.

We'll modify the code so that we can get the page number from the URL:

page.tsx

1import { prisma } from "@/lib/prisma";
2import ProductCard from "../components/ProductCard";
3async function page({ searchParams }: { searchParams: { page: string } }) {
4  const page = Number(searchParams.page) || 1;
5  const data = await prisma.product.findMany({
6    take: 10,
7  });
8  return (
9    <div>
10      <h1 className="text-2xl font-bold text-center my-10">
11        {data.length} Products
12      </h1>
13      <div className="flex flex-wrap gap-10 justify-center my-10">
14        {data.map((product) => (
15          <ProductCard key={product.id} product={product} />
16        ))}
17      </div>
18    </div>
19  );
20}
21
22export default page;
23

What this basically does is that it gets the value of the query parameter "page" and stores it in a variable.

As you can see, this changes absolutely nothing, we're storing the page number but we're doing nothing with it.

So we need to skip a number of products based on the current page, which can be achieved by simply typing: skip: (page-1) *10

if we're on page 1, it will skip 0, on page 2, it will skip 10, and so on...


So that works, but we're still missing something, the actual pagination bar.

Obviously, we're not going to type the page number manually in the URL bar.


To do that, we need to get the total number of products and make an array with the pages' numbers.


page.tsx

1const count = await prisma.product.count(); // Get the total number of products
2  const pages = Array.from(
3    { length: Math.ceil(count / 10) },
4    (_, i) => i + 1
5  );
6  const pagenateArr = (arr: Array<number>, p: number) => {
7    let newArr: Array<number> = [];
8    arr.forEach((element: any) => {
9      if (Math.abs(element - p) <= 2) {
10        newArr = [...newArr, element];
11      }
12    });
13    return newArr;
14  };
15  const Arr = pagenateArr(pages, page);

The above code might seem a bit complicated, it's okay if you don't understand it, I don't either :)

Now all we need to do is to take the array of page numbers that we have and make a pagination component out of it:

page.tsx

1{pages && Arr && (
2        <ol className="flex justify-center gap-1 mt-16 text-sm font-medium">
3          <li>
4            <Link
5              href={{
6                pathname: "/test",
7                query: {
8                  page: pages.at(0),
9                },
10              }}
11              className="inline-flex items-center justify-center w-8 h-8 border border-gray-100 rounded-full hover:bg-slate-400/50 transition "
12            >
13              <svg
14                xmlns="http://www.w3.org/2000/svg"
15                className="w-3 h-3 rotate-180"
16                viewBox="0 0 20 20"
17                fill="currentColor"
18              >
19                <path
20                  fillRule="evenodd"
21                  d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
22                  clipRule="evenodd"
23                />
24              </svg>
25            </Link>
26          </li>
27          {Arr &&
28            Arr.map((page: any) => (
29              <li key={page}>
30                <Link
31                  href={{
32                    pathname: "/test",
33                    query: {
34                      page,
35                    },
36                  }}
37                  className="inline-flex items-center justify-center w-8 h-8 border border-gray-100 rounded-full hover:bg-slate-400/50 transition"
38                >
39                  {page}
40                </Link>
41              </li>
42            ))}
43          <li>
44            <Link
45              href={{
46                pathname: "/test",
47                query: {
48                  page: pages.at(-1),
49                },
50              }}
51              className="inline-flex items-center justify-center w-8 h-8 border border-gray-100 rounded-full hover:bg-slate-400/50 transition"
52            >
53              <svg
54                xmlns="http://www.w3.org/2000/svg"
55                className="w-3 h-3 rotate-180"
56                viewBox="0 0 20 20"
57                fill="currentColor"
58              >
59                <path
60                  fillRule="evenodd"
61                  d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
62                  clipRule="evenodd"
63                />
64              </svg>
65            </Link>
66          </li>
67        </ol>
68      )}

Your full code should look something like this:

page.tsx

1import { prisma } from "@/lib/prisma";
2import ProductCard from "../components/ProductCard";
3import Link from "next/link";
4async function page({ searchParams }: { searchParams: { page: string } }) {
5  const page = Number(searchParams.page) || 1;
6
7  const count = await prisma.product.count(); // Get the total number of products
8  const pages = Array.from({ length: Math.ceil(count / 10) }, (_, i) => i + 1);
9  const pagenateArr = (arr: Array<number>, p: number) => {
10    let newArr: Array<number> = [];
11    arr.forEach((element: any) => {
12      if (Math.abs(element - p) <= 2) {
13        newArr = [...newArr, element];
14      }
15    });
16    return newArr;
17  };
18  const Arr = pagenateArr(pages, page);
19
20  const data = await prisma.product.findMany({
21    skip: (page - 1) * 10,
22    take: 10,
23  });
24  return (
25    <div>
26      <h1 className="text-2xl font-bold text-center my-10">
27        {data.length} Products
28      </h1>
29      <div className="flex flex-wrap gap-10 justify-center my-10">
30        {data.map((product) => (
31          <ProductCard key={product.id} product={product} />
32        ))}
33      </div>
34      {pages && Arr && (
35        <ol className="flex justify-center gap-1 mt-16 text-sm font-medium">
36          <li>
37            <Link
38              href={{
39                pathname: "/test",
40                query: {
41                  page: pages.at(0),
42                },
43              }}
44              className="inline-flex items-center justify-center w-8 h-8 border border-gray-100 rounded-full hover:bg-slate-400/50 transition "
45            >
46              <svg
47                xmlns="http://www.w3.org/2000/svg"
48                className="w-3 h-3 rotate-180"
49                viewBox="0 0 20 20"
50                fill="currentColor"
51              >
52                <path
53                  fillRule="evenodd"
54                  d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
55                  clipRule="evenodd"
56                />
57              </svg>
58            </Link>
59          </li>
60          {Arr &&
61            Arr.map((page: any) => (
62              <li key={page}>
63                <Link
64                  href={{
65                    pathname: "/test",
66                    query: {
67                      page,
68                    },
69                  }}
70                  className="inline-flex items-center justify-center w-8 h-8 border border-gray-100 rounded-full hover:bg-slate-400/50 transition"
71                >
72                  {page}
73                </Link>
74              </li>
75            ))}
76          <li>
77            <Link
78              href={{
79                pathname: "/test",
80                query: {
81                  page: pages.at(-1),
82                },
83              }}
84              className="inline-flex items-center justify-center w-8 h-8 border border-gray-100 rounded-full hover:bg-slate-400/50 transition"
85            >
86              <svg
87                xmlns="http://www.w3.org/2000/svg"
88                className="w-3 h-3 rotate-180"
89                viewBox="0 0 20 20"
90                fill="currentColor"
91              >
92                <path
93                  fillRule="evenodd"
94                  d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
95                  clipRule="evenodd"
96                />
97              </svg>
98            </Link>
99          </li>
100        </ol>
101      )}
102    </div>
103  );
104}
105
106export default page;
107

And as you can see we have a nice pagination at the bottom of the page, you can style however you like

You might also like: