125k

Data Table

TanStack Table で構築された強力なテーブルとデータグリッド。

ステータス
メールアドレス
金額
success
ken99@example.com
$316.00
success
Abe45@example.com
$242.00
processing
Monserrat44@example.com
$837.00
success
Silas22@example.com
$874.00
failed
carmella@example.com
$721.00
5件中0件を選択

はじめに

これまで作成したデータテーブルやデータグリッドはすべて固有のものでした。それぞれ動作が異なり、固有のソートやフィルタリング要件があり、異なるデータソースを扱います。

これらすべてのバリエーションを1つのコンポーネントにまとめるのは適切ではありません。そうすると、ヘッドレス UI が提供する柔軟性が失われます。

そこでデータテーブル コンポーネントそのものではなく、自分で構築する方法を説明するガイドを提供することにしました。

基本的な <Table /> コンポーネントから始め、複雑なデータテーブルをゼロから構築します。

目次

このガイドでは、TanStack Table と <Table /> コンポーネントを使ってカスタム データテーブルを構築する方法を説明します。次のトピックを扱います。

インストール

  1. <Table /> コンポーネントをプロジェクトに追加します。
pnpm dlx shadcn@latest add table
  1. @tanstack/react-table 依存関係を追加します。このガイドでは TanStack Table v9 を使用します。
pnpm add @tanstack/react-table

前提条件

最近の支払いを表示するテーブルを構築します。データは次のようになります。

type Payment = {
  id: string
  amount: number
  status: "pending" | "processing" | "success" | "failed"
  email: string
}
 
export const payments: Payment[] = [
  {
    id: "728ed52f",
    amount: 100,
    status: "pending",
    email: "m@example.com",
  },
  {
    id: "489e1d42",
    amount: 125,
    status: "processing",
    email: "user@example.com",
  },
  // ...
]

プロジェクト構成

次のファイル構成を作成することから始めます。

app
└── payments
    ├── columns.tsx
    ├── data-table-features.ts
    ├── data-table.tsx
    └── page.tsx

ここでは Next.js の例を使用しますが、他の React フレームワークでも動作します。

  • columns.tsx(クライアント コンポーネント)には列定義を記述します。
  • data-table-features.ts には、TanStack Table で有効にする動作を指定する共有 features オブジェクトを記述します。
  • data-table.tsx(クライアント コンポーネント)には <DataTable /> コンポーネントを記述します。
  • page.tsx(サーバー コンポーネント)でデータを取得し、テーブルを描画します。

テーブル機能を設定する

TanStack Table v9 は機能ベースです。tableFeatures() でソート、フィルタリング、ページネーションなど必要な動作を宣言して有効にします。一覧に含めないものはバンドルから tree-shaking されます。組み込みのフィルター関数とソート関数も対象なので、列が使用するものを filterFns と sortFns に登録します(メール フィルターは includesString を使い、文字列列は text / alphanumeric でソートします)。

app/payments/data-table-features.ts
import {
  columnFilteringFeature,
  columnVisibilityFeature,
  createFilteredRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFn_includesString,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  sortFn_alphanumeric,
  sortFn_text,
  tableFeatures,
} from "@tanstack/react-table"
 
// v9 では、このテーブルで使用する機能を宣言します。登録しない機能は
// バンドルから tree-shaking されます。
export const features = tableFeatures({
  columnFilteringFeature,
  columnVisibilityFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  filteredRowModel: createFilteredRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  sortedRowModel: createSortedRowModel(),
  filterFns: { includesString: filterFn_includesString },
  sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
})
 
// Pass this as the first generic argument to `ColumnDef`, `Column`, `Table`,
// and `Row` so each type knows which feature APIs are available.
export type DataTableFeatures = typeof features

基本的なテーブル

基本的なテーブルを構築します。

列の定義

まず列を定義します。

app/payments/columns.tsx
"use client"
 
import { createColumnHelper } from "@tanstack/react-table"
 
import { type DataTableFeatures } from "./data-table-features"
 
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Payment = {
  id: string
  amount: number
  status: "pending" | "processing" | "success" | "failed"
  email: string
}
 
// Use `accessor` for data columns and `display` for columns without one.
const columnHelper = createColumnHelper<DataTableFeatures, Payment>()
 
export const columns = columnHelper.columns([
  columnHelper.accessor("status", {
    header: "ステータス",
  }),
  columnHelper.accessor("email", {
    header: "メールアドレス",
  }),
  columnHelper.accessor("amount", {
    header: "金額",
  }),
])

<DataTable /> コンポーネント

次に、テーブルを描画する <DataTable /> コンポーネントを作成します。

app/payments/data-table.tsx
"use client"
 
import { useTable, type ColumnDef, type RowData } from "@tanstack/react-table"
 
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
 
import { features, type DataTableFeatures } from "./data-table-features"
 
interface DataTableProps<TData extends RowData> {
  columns: ColumnDef<DataTableFeatures, TData>[]
  data: TData[]
}
 
export function DataTable<TData extends RowData>({
  columns,
  data,
}: DataTableProps<TData>) {
  const table = useTable({
    features,
    data,
    columns,
  })
 
  return (
    <div className="overflow-hidden rounded-md border">
      <Table>
        <TableHeader>
          {table.getFlatHeaders().map((header) => (
            <TableHead
              key={header.id}
              id={header.id}
              isRowHeader={header.index === 0}
            >
              {header.isPlaceholder ? null : (
                <table.FlexRender header={header} />
              )}
            </TableHead>
          ))}
        </TableHeader>
        <TableBody renderEmptyState={() => "結果がありません。"}>
          {table.getRowModel().rows.map((row) => (
            <TableRow key={row.id} id={row.id}>
              {row.getVisibleCells().map((cell) => (
                <TableCell key={cell.id}>
                  <table.FlexRender cell={cell} />
                </TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

テーブルをレンダリングする

最後に、ページ コンポーネントでテーブルをレンダリングします。

app/payments/page.tsx
import { columns, Payment } from "./columns"
import { DataTable } from "./data-table"
 
async function getData(): Promise<Payment[]> {
  // ここで API からデータを取得します。
  return [
    {
      id: "728ed52f",
      amount: 100,
      status: "pending",
      email: "m@example.com",
    },
    // ...
  ]
}
 
export default async function DemoPage() {
  const data = await getData()
 
  return (
    <div className="container mx-auto py-10">
      <DataTable columns={columns} data={data} />
    </div>
  )
}

セルの書式設定

金額セルをドル表示に書式設定し、右揃えにします。

列の定義を更新する

金額の header と cell の定義を次のように更新します。

app/payments/columns.tsx
export const columns = columnHelper.columns([
  columnHelper.accessor("amount", {
    header: () => <div className="text-right">金額</div>,
    cell: ({ row }) => {
      const amount = parseFloat(row.getValue("amount"))
      const formatted = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
      }).format(amount)
 
      return <div className="text-right font-medium">{formatted}</div>
    },
  }),
])

同じ方法で、他のセルやヘッダーも書式設定できます。

行のアクション

テーブルに行のアクションを追加します。ここでは <Dropdown /> コンポーネントを使用します。

列定義を更新

列の定義を更新して新しい actions 列を追加します。actions セルは <Dropdown /> コンポーネントを返します。

app/payments/columns.tsx
"use client"
 
import { createColumnHelper } from "@tanstack/react-table"
import { MoreHorizontal } from "lucide-react"
 
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
 
export const columns = columnHelper.columns([
  // ...
  columnHelper.display({
    id: "actions",
    cell: ({ row }) => {
      const payment = row.original
 
      return (
        <DropdownMenuTrigger>
          <Button variant="ghost" size="icon-xs">
            <span className="sr-only">メニューを開く</span>
            <MoreHorizontal />
          </Button>
          <DropdownMenu placement="bottom end" className="w-44">
            <DropdownMenuGroup>
              <DropdownMenuLabel>Actions</DropdownMenuLabel>
              <DropdownMenuItem
                onClick={() => navigator.clipboard.writeText(payment.id)}
              >
                Copy payment ID
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuGroup>
              <DropdownMenuItem>View customer</DropdownMenuItem>
              <DropdownMenuItem>View payment details</DropdownMenuItem>
            </DropdownMenuGroup>
          </DropdownMenu>
        </DropdownMenuTrigger>
      )
    },
  }),
  // ...
])

cell 関数では row.original を使って行のデータにアクセスできます。これを使って行のアクションを処理します。たとえば id を使って API に DELETE リクエストを送信できます。

ページネーション

次に、テーブルへページネーションを追加します。

ページネーションはすでに有効

features オブジェクトには rowPaginationFeature と createPaginatedRowModel() が含まれているため、テーブルは行を自動的に 10 件ずつのページに分割します。useTable に追加するものはありません。ページ サイズのカスタマイズや手動ページネーションについては、ページネーションのドキュメントを参照してください。

ページネーションコントロールを追加する

<Button /> コンポーネントと table.previousPage()、table.nextPage() API メソッドを使って、テーブルにページネーション コントロールを追加できます。

app/payments/data-table.tsx
import { Button } from "@/components/ui/button"
 
export function DataTable<TData extends RowData>({
  columns,
  data,
}: DataTableProps<TData>) {
  const table = useTable({
    features,
    data,
    columns,
  })
 
  return (
    <div>
      <div className="overflow-hidden rounded-md border">
        <Table>
          { // .... }
        </Table>
      </div>
      <div className="flex items-center justify-end space-x-2 py-4">
        <Button
          variant="outline"
          size="sm"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          Previous
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          Next
        </Button>
      </div>
    </div>
  )
}

より高度なページネーション コンポーネントについては、再利用可能なコンポーネントを参照してください。

並べ替え

メールアドレス列を並べ替え可能にします。

rowSortingFeature とソート済み行モデルはすでに features オブジェクトに登録されています。あとは並べ替えの状態を接続するだけです。

<DataTable> を更新する

app/payments/data-table.tsx
"use client"
 
import * as React from "react"
import {
  useTable,
  type ColumnDef,
  type RowData,
  type SortingState,
} from "@tanstack/react-table"
 
export function DataTable<TData extends RowData>({
  columns,
  data,
}: DataTableProps<TData>) {
  const [sorting, setSorting] = React.useState<SortingState>([])
 
  const table = useTable({
    features,
    data,
    columns,
    onSortingChange: setSorting,
    state: {
      sorting,
    },
  })
 
  return (
    <div>
      <div className="overflow-hidden rounded-md border">
        <Table
          sortDescriptor={
            sorting.length
              ? {
                  column: sorting[0].id,
                  direction: sorting[0].desc ? "descending" : "ascending",
                }
              : undefined
          }
          onSortChange={(sortDescriptor) => {
            table.setSorting([
              {
                id: "" + sortDescriptor.column,
                desc: sortDescriptor.direction === "descending",
              },
            ]);
          }}>
          { ... }
        </Table>
      </div>
    </div>
  )
}

ヘッダーセルを並べ替え可能にする

これで email ヘッダー セルを更新し、並べ替えコントロールを追加できます。

app/payments/columns.tsx
"use client"
 
import { createColumnHelper } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
 
import { buttonVariants } from "@/components/ui/button"
 
export const columns = columnHelper.columns([
  columnHelper.accessor("email", {
    header: ({ column }) => {
      return (
        <div className={buttonVariants({ variant: "ghost" })}>
          Email
          <ArrowUpDown className="ml-2 h-4 w-4" />
        </div>
      )
    },
  }),
])

ヘッダー セルを切り替えると、テーブルが自動的に昇順・降順で並べ替えられます。

フィルタリング

テーブルのメールアドレスを絞り込む検索入力を追加します。

columnFilteringFeature とフィルター済み行モデルはすでに features オブジェクトに含まれているため、フィルター状態を接続して入力欄を表示するだけで済みます。

<DataTable> を更新する

app/payments/data-table.tsx
"use client"
 
import * as React from "react"
import {
  useTable,
  type ColumnDef,
  type ColumnFiltersState,
  type RowData,
  type SortingState,
} from "@tanstack/react-table"
 
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
 
export function DataTable<TData extends RowData>({
  columns,
  data,
}: DataTableProps<TData>) {
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    []
  )
 
  const table = useTable({
    features,
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    state: {
      sorting,
      columnFilters,
    },
  })
 
  return (
    <div>
      <div className="flex items-center py-4">
        <Input
          placeholder="メールアドレスを絞り込む..."
          value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
          onChange={(event) =>
            table.getColumn("email")?.setFilterValue(event.target.value)
          }
          className="max-w-sm"
        />
      </div>
      <div className="overflow-hidden rounded-md border">
        <Table>{ ... }</Table>
      </div>
    </div>
  )
}

これで email 列のフィルタリングが有効になりました。他の列にもフィルターを追加できます。フィルターのカスタマイズについては、フィルタリングのドキュメントを参照してください。

表示状態

@tanstack/react-table の表示状態 API を使えば、列の表示・非表示を簡単に追加できます。

<DataTable> を更新する

app/payments/data-table.tsx
"use client"
 
import * as React from "react"
import {
  useTable,
  type ColumnDef,
  type ColumnFiltersState,
  type ColumnVisibilityState,
  type RowData,
  type SortingState,
} from "@tanstack/react-table"
 
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
 
export function DataTable<TData extends RowData>({
  columns,
  data,
}: DataTableProps<TData>) {
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    []
  )
  const [columnVisibility, setColumnVisibility] =
    React.useState<ColumnVisibilityState>({})
 
  const table = useTable({
    features,
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
    },
  })
 
  return (
    <div>
      <div className="flex items-center py-4">
        <Input
          placeholder="メールアドレスを絞り込む..."
          value={table.getColumn("email")?.getFilterValue() as string}
          onChange={(event) =>
            table.getColumn("email")?.setFilterValue(event.target.value)
          }
          className="max-w-sm"
        />
        <DropdownMenuTrigger>
          <Button variant="outline" className="ml-auto">
            Columns
          </Button>
          <DropdownMenu placement="bottom end">
            <DropdownMenuGroup
              selectionMode="multiple"
              selectedKeys={
                table
                  .getVisibleFlatColumns()
                  .filter((column) => column.getCanHide())
                  .map(column => column.id)
              }
              onSelectionChange={(keys) => {
                table.setColumnVisibility(
                  Object.fromEntries(
                    table
                      .getAllFlatColumns()
                      .map((c) => [
                        c.id,
                        !c.getCanHide() || keys === "all" || keys.has(c.id),
                      ])
                  )
                )
              }}
            >
              {table
                .getAllColumns()
                .filter((column) => column.getCanHide())
                .map((column) => {
                  return (
                    <DropdownMenuItem
                      key={column.id}
                      id={column.id}
                      className="capitalize"
                    >
                      {column.id}
                    </DropdownMenuItem>
                  )
                })}
            </DropdownMenuGroup>
          </DropdownMenu>
        </DropdownMenuTrigger>
      </div>
      <div className="overflow-hidden rounded-md border">
        <Table>{ ... }</Table>
      </div>
    </div>
  )
}

これで、列の表示・非表示を切り替えるドロップダウン メニューが追加されます。

行の選択

次に、テーブルへ行の選択を追加します。

列定義を更新

app/payments/columns.tsx
"use client"
 
import { createColumnHelper } from "@tanstack/react-table"
 
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
 
export const columns = columnHelper.columns([
  columnHelper.display({
    id: "select",
    header: () => <Checkbox slot="selection" />,
    cell: () => <Checkbox slot="selection" />,
    enableSorting: false,
    enableHiding: false,
  }),
])

<DataTable> を更新する

app/payments/data-table.tsx
export function DataTable<TData extends RowData>({
  columns,
  data,
}: DataTableProps<TData>) {
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    []
  )
  const [columnVisibility, setColumnVisibility] =
    React.useState<ColumnVisibilityState>({})
  const [rowSelection, setRowSelection] = React.useState({})
 
  const table = useTable({
    features,
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
    },
  })
 
  return (
    <div>
      <div className="overflow-hidden rounded-md border">
        <Table
          selectionMode="multiple"
          selectedKeys={table.getSelectedRowModel().rows.map((row) => row.id)}
          onSelectionChange={(selection) => {
            if (selection === "all") {
              table.toggleAllRowsSelected()
            } else {
              table.setRowSelection(
                Object.fromEntries([...selection].map((key) => [key, true]))
              )
            }
          }}
        />
      </div>
    </div>
  )
}

各行にチェックボックスが追加され、ヘッダーのチェックボックスで全行を選択できるようになります。

選択した行を表示

table.getFilteredSelectedRowModel() API を使って、選択した行数を表示できます。

<div className="flex-1 text-sm text-muted-foreground">
  {table.getFilteredSelectedRowModel().rows.length} of{" "}
  {table.getFilteredRowModel().rows.length} row(s) selected.
</div>

再利用可能なコンポーネント

データテーブルの構築に使えるコンポーネントを紹介します。これは Tasks デモのもので、data-table-features.ts モジュールを通じて、すべてのコンポーネントで features オブジェクト(対応する TasksTableFeatures 型も含む)を共有しています。テーブル機能の設定で構築したものと同じパターンです。

列ヘッダー

任意の列ヘッダーを並べ替え可能・非表示可能にします。

export const columns = columnHelper.columns([
  columnHelper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="メールアドレス" />
    ),
  }),
])

ページネーション

ページ サイズと選択数を含むページネーション コントロールをテーブルに追加します。

<DataTablePagination table={table} />

列の切り替え

列の表示・非表示を切り替えるコンポーネントです。

<DataTableViewOptions table={table} />

RTL

shadcn/ui で RTL サポートを有効にするには、RTL 構成ガイドを参照してください。

الحالة
البريد الإلكتروني
المبلغ
ناجح
ken99@example.com
‏٣١٦٫٠٠ US$
ناجح
Abe45@example.com
‏٢٤٢٫٠٠ US$
قيد المعالجة
Monserrat44@example.com
‏٨٣٧٫٠٠ US$
ناجح
Silas22@example.com
‏٨٧٤٫٠٠ US$
فشل
carmella@example.com
‏٧٢١٫٠٠ US$
0 من 5 صف(وف) محدد.