New product features | The latest in technology | The weekly debugging nightmares & more!
July 24, 2023
Take my knowledge and shove it up your brain...
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:
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.
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
Next js 13 group routes is a game changer
This is a new feature in next js 13 that solves so many problems
Metadata guide in Next js 13
Next 13 introduced a new way to optimize your metadata, here's what you need to know
Next js 13 Route handlers explained
Forget the old api routes, this is the way
All you need to know about useOptimistic hook in Next js13
A new experimental feature from Next js 13 that will greatly imporove your UX