はじめに
Webアプリの開発において、ページネーションはユーザーにとって使いやすいインターフェースを提供するために頻繁に使用されます。今回はNext.jsを用いて、userテーブルから取得したユーザー情報に対してページネーションを実装する方法を紹介します。
階層図
Project
│
├── src
│ ├── app
│ │ ├── api
│ │ │ └── user
│ │ │ └── route.ts // バックエンドのAPIルート
│ │ └── user
│ │ └── page.tsx // ユーザー一覧ページ(フロントエンドのエントリポイント)
バックエンド部分の実装
・全体件数を取得。
・1ページに表示するデータの最大件数を設定。
・開始位置に応じた表示データを取得。
・ページ数を計算。(全体件数/1ページに表示するデータの最大件数)
バックエンド部分のソースコード
src\app\api\user\route.ts
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function GET(req: NextRequest) {
//クエリパラメータからページ番号を取得し、整数に変換(デフォルトは1)
const page = parseInt(req.nextUrl.searchParams.get('page') || '1', 10);
//クエリパラメータからページごとの表示数を取得し、整数に変換(デフォルトは10)
const limit = parseInt(req.nextUrl.searchParams.get('limit') || '10', 10);
//検索の開始位置を取得。
const offset = (page - 1) * limit;
try {
const data = await prisma.user.findMany({
skip: offset,
take: limit,
select:{
id:true,
name:true,
email:true
}
});
const totalItems = await prisma.user.count();
const totalPages = Math.ceil(totalItems / limit);
return NextResponse.json({
items: data,
totalPages,
currentPage: page,
}, { status: 200 });
} catch (error:any) {
console.error("Error fetching data: ", error);
return new NextResponse(JSON.stringify({ message: 'Error', error: error.message }), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
} finally {
await prisma.$disconnect();
}
}
全体件数を取得:
・全体件数を取得するクエリを実行します。
const totalItems = await prisma.user.count();
1ページに表示するデータの最大件数を設定:
・ページごとの表示件数(limit)を設定します。
const limit = parseInt(req.nextUrl.searchParams.get('limit') || '10', 10);
開始位置に応じた表示データを取得
Prismaでデータを取得する際には、skip と take というオプションを使用して、どこからデータを取得し、何件取得するかを指定します。
例えば、全体の取得対象データが50件あり、1ページに表示するデータの上限数が10件、そして2ページ目を表示する場合です。このとき、次のように計算します。
・skip オプションには offset を設定します。offset は (2 - 1) * 10 = 10 となり、最初の10件をスキップします。
・take オプションには 10 を設定し、スキップされた後の11件目から20件目までのデータを10件取得します。
const page = 2; // 例: 2ページ目
const limit = 10; // 1ページに表示するデータの数
const offset = (page - 1) * limit; // スキップするデータの数
const data = await prisma.user.findMany({
skip: offset, // ここでは最初の10件をスキップ
take: limit, // 11件目から20件目までの10件を取得
select: {
id: true,
name: true,
email: true,
},
});
ページ数を計算:
・全体件数と1ページあたりの件数を基に総ページ数を計算します。
const totalPages = Math.ceil(totalItems / limit);
フロントエンド部分の実装
①データのフェッチ
②ページネーションの生成
③前へ・次へボタンの表示
④対象ページのクリック処理
フロントエンド部分のソースコード
"use client"
import React, { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
export default function Top() {
const router = useRouter();
const searchParams = useSearchParams();
const [data, setData] = useState([]);
const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
async function fetchData(page) {
try {
const response = await fetch(`/api/user?page=${page}`);
if (!response.ok) {
throw new Error("Bad response");
}
const newData = await response.json();
setTotalPages(newData.totalPages);
setCurrentPage(newData.currentPage);
setData(newData.items);
} catch (error) {
console.error("Failed:", error);
}
}
useEffect(() => {
const page = parseInt(searchParams.get('page') || '1', 10);
fetchData(page);
}, [searchParams]);
const generatePagination = () => {
const pages = [];
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
return pages;
};
const handlePageChange = (page) => {
router.push(`/user?page=${page}`);
setCurrentPage(page);
};
return (
<>
<main className="bg-home-bg bg-no-repeat bg-cover bg-center flex flex-col items-center justify-between p-4 sm:p-8 md:p-24 min-h-screen">
<div className="text-center mb-4 max-w-4xl">
<h1 className="mt-20 md:mt-0 text-white text-2xl sm:text-5xl md:text-6xl lg:text-4xl font-bold text-shadow-lg mb-4">
ユーザーリスト
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{data.map(item => (
<p className="text-white text-lg sm:text-xl md:text-2xl text-shadow-md mt-8 mb-16" key={item.id}>
{item.name}
</p>
))}
</div>
</div>
{totalPages > 1 && (
<div className="flex justify-center w-full max-w-5xl mt-6 space-x-2">
{currentPage > 1 && (
<button
onClick={() => handlePageChange(currentPage - 1)}
className="bg-gray-300 text-black px-4 py-2 sm:px-6 sm:py-3 rounded text-sm sm:text-base w-20 h-10 sm:w-24 sm:h-12 hover:bg-gray-400"
>
前へ
</button>
)}
<div className="flex justify-center space-x-2">
{generatePagination().map((page, index) => (
<button
key={index}
onClick={() => typeof page === 'number' && handlePageChange(page)}
className={`w-10 h-10 sm:w-12 sm:h-12 mx-1 rounded text-sm sm:text-base ${currentPage === page ? 'bg-blue-500 text-white' : 'bg-gray-300 text-black hover:bg-gray-400'}`}
disabled={typeof page !== 'number'}
>
{page}
</button>
))}
</div>
{currentPage < totalPages && (
<button
onClick={() => handlePageChange(currentPage + 1)}
className="bg-gray-300 text-black px-4 py-2 sm:px-6 sm:py-3 rounded text-sm sm:text-base w-20 h-10 sm:w-24 sm:h-12 hover:bg-gray-400"
>
次へ
</button>
)}
</div>
)}
</main>
</>
);
}
データのフェッチ
async function fetchData(page) {
try {
const response = await fetch(`/api/user?page=${page}`);
if (!response.ok) {
throw new Error("Bad response");
}
const newData = await response.json();
setTotalPages(newData.totalPages);
setCurrentPage(newData.currentPage);
setData(newData.items);
} catch (error) {
console.error("Failed:", error);
}
}
バックエンドで取得した全体ページ数、現在のページを状態変数に格納します。
全体のページ数を表示
const generatePagination = () => {
const pages = [];
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
return pages;
};
1から totalPages までの数字を pages 配列に追加します。
最終的に pages 配列を返すことで、ページネーションのリンクを動的に生成します。
この generatePagination 関数によって、ユーザーは全てのページ番号を確認でき、任意のページ番号をクリックして移動することができます。
前へボタン,次へボタンの表示、非表示について
現在のページの値が1より大きい場合、前ボタンを表示します。
{currentPage > 1 && (
<button
onClick={() => handlePageChange(currentPage - 1)}
className="bg-gray-300 text-black px-4 py-2 sm:px-6 sm:py-3 rounded text-sm sm:text-base w-20 h-10 sm:w-24 sm:h-12 hover:bg-gray-400"
>
前へ
</button>
)}
総ページの値が現在のページの値より大きい場合、次へボタンを表示します。
{currentPage < totalPages && (
<button
onClick={() => handlePageChange(currentPage + 1)}
className="bg-gray-300 text-black px-4 py-2 sm:px-6 sm:py-3 rounded text-sm sm:text-base w-20 h-10 sm:w-24 sm:h-12 hover:bg-gray-400"
>
次へ
</button>
)}
対象ページクリック時の状態
対象ページをクリックし、setCurrentPageに状態変数として、ページを格納する。
const handlePageChange = (page) => {
router.push(`/user?page=${page}`);
setCurrentPage(page);
};
まとめ
いかがだったでしょうか。今回はnext.jsにおけるページネーションについて
まとめてみました。
userテーブルから取得したユーザー情報に対して、バックエンドとフロントエンドの両方でページネーションを行う方法を紹介しました。
今回の内容が、Next.jsを使用する上での理解を深める手助けとなれば幸いです。今後も最新の技術をキャッチアップしながら、発信を続けていきたいと思います。