React – shadcn/ui の Table でページネーション (Pagination) の実装方法を解説

概要

shadcn/ui の Table コンポーネントにページネーション (Pagination) を実装する方法を解説します。

前提

以下のコードは React – shadcn/ui の Table コンポーネントについて解説 | pystyle で作成した動的なテーブルの実装の機能追加になります。 まず、上記のページでテーブルを実装し、そのコードに以下のコードを追加してください。

Pagination を実装する

Pagination の実装例

1. Button コンポーネントを追加

ページ遷移ボタンの実装には、Button コンポーネント、Select コンポーネントを使用するため、インストールします。

npx shadcn@latest add button
npx shadcn@latest add select
Bash

2. ページネーションの実装

src/components/ui/pagenation.tsx を作成し、以下の内容を記載します。

import { Table } from "@tanstack/react-table";
import {
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
} from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

interface DataTablePaginationProps<TData> {
  table: Table<TData>;
  className?: string;
}

export function DataTablePagination<TData>({
  table,
  className,
}: DataTablePaginationProps<TData>) {
  return (
    <div className={cn("flex justify-between items-center", className)}>
      {/* ページ ${CurrentPage} / ${TotalPage} (${CurrentRow} 件中 ${TotalRow} 件) */}
      <div className="font-medium text-sm">
        {`ページ ${
          table.getState().pagination.pageIndex + 1
        } / ${table.getPageCount()} (${
          table.getState().pagination.pageSize *
          (table.getState().pagination.pageIndex + 1)
        } 件 / ${table.getRowCount()} 件)`}
      </div>

      <div className="flex items-center space-x-6">
        {/* 表示行数 */}
        <div className="flex items-center space-x-2">
          <p className="font-medium text-sm">表示件数</p>
          <Select
            value={`${table.getState().pagination.pageSize}`}
            onValueChange={(value) => {
              table.setPageSize(Number(value));
            }}
          >
            <SelectTrigger className="w-[70px] h-8">
              <SelectValue placeholder={table.getState().pagination.pageSize} />
            </SelectTrigger>
            <SelectContent side="top">
              {[10, 20, 30, 40, 50].map((pageSize) => (
                <SelectItem key={pageSize} value={`${pageSize}`}>
                  {pageSize}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>

        {/* ページ遷移ボタン */}
        <div className="flex border rounded-md [&_button]:rounded-none divide-x [&_button]:w-9">
          {/* 最初へ */}
          <Button
            variant="ghost"
            size="sm"
            onClick={table.firstPage}
            disabled={!table.getCanPreviousPage()}
          >
            <span className="sr-only">最初へ</span>
            <ChevronsLeft />
          </Button>
          {/* 前へ */}
          <Button
            variant="ghost"
            size="sm"
            onClick={table.previousPage}
            disabled={!table.getCanPreviousPage()}
          >
            <span className="sr-only">前へ</span>
            <ChevronLeft />
          </Button>
          {/* 前後2ページ分のボタン */}
          {Array.from({ length: table.getPageCount() }, (_, i) => i)
            .filter(
              (pageIndex) =>
                pageIndex >= table.getState().pagination.pageIndex - 2 &&
                pageIndex <= table.getState().pagination.pageIndex + 2
            )
            .map((pageIndex) => (
              <Button
                key={pageIndex}
                variant="ghost"
                size="sm"
                className={cn(
                  "disabled:opacity-100",
                  table.getState().pagination.pageIndex === pageIndex &&
                    "bg-gray-300"
                )}
                onClick={() => table.setPageIndex(pageIndex)}
                disabled={table.getState().pagination.pageIndex === pageIndex}
              >
                {pageIndex + 1}
              </Button>
            ))}
          {/* 次へ */}
          <Button
            variant="ghost"
            size="sm"
            onClick={table.nextPage}
            disabled={!table.getCanNextPage()}
          >
            <span className="sr-only">次へ</span>
            <ChevronRight />
          </Button>
          {/* 最後へ */}
          <Button
            variant="ghost"
            size="sm"
            onClick={table.lastPage}
            disabled={!table.getCanNextPage()}
          >
            <span className="sr-only">最後へ</span>
            <ChevronsRight />
          </Button>
        </div>
      </div>
    </div>
  );
}
React TSX

解説

ページ 5 / 100 (10 件中 1000 件) のように、現在のページ数、総ページ数、現在の表示件数、総件数を表示します。 それぞれの値は、以下のように取得できます。

  • 1 ページあたりの表示行数: table.getState().pagination.pageSize
  • 現在表示中のページ: table.getState().pagination.pageIndex + 1
  • 現在表示中のページの最終行: table.getState().pagination.pageSize * (table.getState().pagination.pageIndex + 1)
  • 総行数: table.getRowCount()
<div className="font-medium text-sm">
  {`ページ ${
    table.getState().pagination.pageIndex + 1
  } / ${table.getPageCount()} (${
    table.getState().pagination.pageSize *
    (table.getState().pagination.pageIndex + 1)
  } 件 / ${table.getRowCount()} 件)`}
</div>
React TSX

Select コンポーネントを使用して、1 ページあたりの表示行数を変更できるようにします。

<div className="flex items-center space-x-2">
  <p className="font-medium text-sm">表示件数</p>
  <Select
    value={`${table.getState().pagination.pageSize}`}
    onValueChange={(value) => {
      table.setPageSize(Number(value));
    }}
  >
    <SelectTrigger className="w-[70px] h-8">
      <SelectValue placeholder={table.getState().pagination.pageSize} />
    </SelectTrigger>
    <SelectContent side="top">
      {[10, 20, 30, 40, 50].map((pageSize) => (
        <SelectItem key={pageSize} value={`${pageSize}`}>
          {pageSize}
        </SelectItem>
      ))}
    </SelectContent>
  </Select>
</div>
React TSX

最初のページに遷移するには table.firstPage() を呼び出します。 先頭のページでは最初のページに遷移できないため、table.getCanPreviousPage()false の場合は、ボタンを無効にします。

<Button
  variant="ghost"
  size="sm"
  onClick={table.firstPage}
  disabled={!table.getCanPreviousPage()}
>
  <span className="sr-only">最初へ</span>
  <ChevronsLeft />
</Button>
React TSX

前のページに遷移するには table.previousPage() を呼び出します。 先頭のページでは前のページに遷移できないため、table.getCanPreviousPage()false の場合は、ボタンを無効にします。

<Button
  variant="ghost"
  size="sm"
  onClick={table.previousPage}
  disabled={!table.getCanPreviousPage()}
>
  <span className="sr-only">前へ</span>
  <ChevronLeft />
</Button>
React TSX

現在のページの前後 2 ページへ遷移できるボタンを表示します。 現在のページのボタンはクリックできないように無効にし、背景色を変更して強調します。

{
  Array.from({ length: table.getPageCount() }, (_, i) => i)
    .filter(
      (pageIndex) =>
        pageIndex >= table.getState().pagination.pageIndex - 2 &&
        pageIndex <= table.getState().pagination.pageIndex + 2
    )
    .map((pageIndex) => (
      <Button
        key={pageIndex}
        variant="ghost"
        size="sm"
        className={cn(
          "disabled:opacity-100",
          table.getState().pagination.pageIndex === pageIndex && "bg-gray-300"
        )}
        onClick={() => table.setPageIndex(pageIndex)}
        disabled={table.getState().pagination.pageIndex === pageIndex}
      >
        {pageIndex + 1}
      </Button>
    ));
}
React TSX

次のページに遷移するには table.nextPage() を呼び出します。 最後のページでは次のページに遷移できないため、table.getCanNextPage()false の場合は、ボタンを無効にします。

<Button
  variant="ghost"
  size="sm"
  onClick={table.nextPage}
  disabled={!table.getCanNextPage()}
>
  <span className="sr-only">次へ</span>
  <ChevronRight />
</Button>
React TSX

最後のページに遷移するには table.lastPage() を呼び出します。 最後のページでは次のページに遷移できないため、table.getCanNextPage()false の場合は、ボタンを無効にします。

<Button
  variant="ghost"
  size="sm"
  onClick={table.lastPage}
  disabled={!table.getCanNextPage()}
>
  <span className="sr-only">最後へ</span>
  <ChevronsRight />
</Button>
React TSX

3. ページネーションの追加

テーブルを実装した src/payment/payment-table.tsx を以下のように変更します。

src/payment/payment-table.tsx
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,  // 追加
  useReactTable,
} from "@tanstack/react-table";

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { DataTablePagination } from "@/components/ui/pagenation";  // 追加

interface PaymentTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function PaymentTable<TData, TValue>({
  columns,
  data,
}: PaymentTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),  // 追加
  });

  return (
    <div>
      <div className="border rounded-md">
        <Table>
          {/* Header */}
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead key={header.id}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>

          {/* Body */}
          <TableBody>
            {table.getRowModel().rows?.length ? (
              // データが存在する場合
              table.getRowModel().rows.map((row) => (
                <TableRow
                  key={row.id}
                  data-state={row.getIsSelected() && "selected"}
                >
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              // データが存在しない場合
              <TableRow>
                <TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                >
                  データなし
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>

      {/* Pagination */}
      <DataTablePagination table={table} className="mt-3" /> {/* 追加 */}
    </div>
  );
}
React TSX

コメント

コメントする