0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js ページネーションの実装についてまとめてみた

Posted at

はじめに

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,
  },
});

公式ドキュメント
https://www-prisma-io.translate.goog/docs/orm/prisma-client/queries/pagination?_x_tr_sl=en&_x_tr_tl=ja&_x_tr_hl=ja&_x_tr_pto=sc

ページ数を計算:

・全体件数と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を使用する上での理解を深める手助けとなれば幸いです。今後も最新の技術をキャッチアップしながら、発信を続けていきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?