TanStack Tableを使ったテーブルUI構築について



今回はモダンな Web 開発におけるライブラリ「TanStack」について、
そしてその中の一つである複雑なテーブル UI を構築するための強力なライブラリ「TanStack Table」について記載していきます。
TanStack Table は、ソート、フィルタリング、ページネーション、行選択など、テーブルに必要な機能を簡単に実装できる「ヘッドレス UI」ライブラリです。UI を持たないため、デザインは完全に自由にカスタマイズできます。
この記事では、基本的な概念から実際に動くテーブルの構築まで記していきます。
TanStack とは
その前に TanStack1 とは何かというと、Web 開発における様々な課題を解決するためのライブラリ群です。元々は「React Query」という名前で React のデータフェッチライブラリとして知られていましたが、現在はフレームワーク非依存の汎用的なツール群として進化しています。
重要なポイント
- TanStack は単一のライブラリではなく、複数のライブラリの総称です
- もともとは React 専用でしたが、現在は Vue、Solid、Svelte など様々なフレームワークに対応しています
- 開発者は Tanner Linsley 氏で、彼の名前から「TanStack」と名付けられました
TanStack Table とは
TanStack Table2 はその中の一つで、テーブル UI のロジックを提供するヘッドレス UI ライブラリです。React、Vue、Solid、Svelte など、様々なフレームワークに対応しています。
ここでいう「ヘッドレス」とは、UI を持たず、ロジックだけを提供するという意味です。
従来のUIライブラリ:
┌─────────────────────────────┐
│ ロジック + スタイル(固定) │
└─────────────────────────────┘
↓ カスタマイズが難しい
ヘッドレスUIライブラリ:
┌─────────────────────────────┐
│ ロジックのみ │
└─────────────────────────────┘
+
┌─────────────────────────────┐
│ スタイル(自由にカスタマイズ)│
└─────────────────────────────┘
↓ 完全な自由度
特徴としては、主に以下があります。
- 完全なカスタマイズ性: TailwindCSS、Material-UI、独自 CSS など、好きなスタイルを適用可能
- 豊富な機能: ソート、フィルタリング、ページネーション、行選択、列のリサイズなど
- 型安全: TypeScript との親和性が高い
- 軽量: 必要な機能だけをインポートできる
- フレームワーク非依存: React、Vue、Solid、Svelte に対応
なぜ TanStack Table が必要なのか
従来の方法では、ソート・フィルタリング・ページネーションなどを毎回自前で実装する必要がありました。
// 従来の方法(すべて自分で実装)
function UserTable({ users }) {
const [sortColumn, setSortColumn] = useState(null);
const [sortDirection, setSortDirection] = useState("asc");
const [filter, setFilter] = useState("");
const [currentPage, setCurrentPage] = useState(1);
// ソート、フィルタリング、ページネーションのロジックを
// すべて自分で実装する必要がある...
}
TanStack Table を使えば、これらの機能を宣言的に追加できます
// TanStack Tableを使った方法
function UserTable({ users }) {
const table = useReactTable({
data: users,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
// あとはテーブルをレンダリングするだけ
}
TanStack Table の主要概念
Tanstack Table で利用する概念としては以下があります。
| 概念 | 説明 |
|---|---|
| Column Definition | テーブルの各列を定義。ヘッダー、セルの表示方法、ソート可能かどうかなどを指定 |
| Row Model | テーブルの行データを管理。ソート、フィルタリング、ページネーションなどの処理を行う |
| Table Instance | テーブル全体の状態と操作を管理するオブジェクト |
| Cell | テーブルの各セルを表す。値の取得やレンダリングを行う |
| Header | テーブルのヘッダー行を表す。ソートボタンやフィルター入力などを配置 |
実践ハンズオン - ユーザー管理テーブルを作ろう
それでは、実際に TanStack Table を使って、ソート、フィルタリング、ページネーション、行選択機能を持つユーザー管理テーブルを構築してみましょう。
前提条件
- Node.js(v18 以上)がインストールされていること
- React の基本的な知識があること
- TypeScript の基礎知識があること
プロジェクトのセットアップ
以下で今回利用するプロジェクトを作成します。
# Viteを使ってReact + TypeScriptプロジェクトを作成
npm create vite@latest tanstack-table-demo -- --template react-ts
cd tanstack-table-demo
npm install
# TanStack Tableをインストール
npm install @tanstack/react-table
型定義とサンプルデータの作成
src/data/users.tsを作成します。
// ユーザーの型定義
export interface User {
id: number;
firstName: string;
lastName: string;
email: string;
age: number;
status: "active" | "inactive" | "pending";
role: "admin" | "user" | "guest";
createdAt: string;
}
// サンプルデータ
export const users: User[] = [
{
id: 1,
firstName: "太郎",
lastName: "山田",
email: "taro.yamada@example.com",
age: 28,
status: "active",
role: "admin",
createdAt: "2024-01-15",
},
{
id: 2,
firstName: "花子",
lastName: "鈴木",
email: "hanako.suzuki@example.com",
age: 34,
status: "active",
role: "user",
createdAt: "2024-02-20",
},
{
id: 3,
firstName: "一郎",
lastName: "佐藤",
email: "ichiro.sato@example.com",
age: 45,
status: "inactive",
role: "user",
createdAt: "2023-11-10",
},
{
id: 4,
firstName: "美咲",
lastName: "田中",
email: "misaki.tanaka@example.com",
age: 23,
status: "pending",
role: "guest",
createdAt: "2024-03-05",
},
{
id: 5,
firstName: "健太",
lastName: "高橋",
email: "kenta.takahashi@example.com",
age: 31,
status: "active",
role: "user",
createdAt: "2024-01-28",
},
{
id: 6,
firstName: "由美",
lastName: "伊藤",
email: "yumi.ito@example.com",
age: 29,
status: "active",
role: "admin",
createdAt: "2023-12-15",
},
{
id: 7,
firstName: "大輔",
lastName: "渡辺",
email: "daisuke.watanabe@example.com",
age: 38,
status: "inactive",
role: "user",
createdAt: "2023-10-20",
},
{
id: 8,
firstName: "愛",
lastName: "小林",
email: "ai.kobayashi@example.com",
age: 26,
status: "active",
role: "user",
createdAt: "2024-02-10",
},
{
id: 9,
firstName: "翔太",
lastName: "加藤",
email: "shota.kato@example.com",
age: 33,
status: "pending",
role: "guest",
createdAt: "2024-03-01",
},
{
id: 10,
firstName: "真由",
lastName: "吉田",
email: "mayu.yoshida@example.com",
age: 27,
status: "active",
role: "user",
createdAt: "2024-01-05",
},
{
id: 11,
firstName: "隆",
lastName: "山本",
email: "takashi.yamamoto@example.com",
age: 42,
status: "active",
role: "admin",
createdAt: "2023-09-15",
},
{
id: 12,
firstName: "恵",
lastName: "中村",
email: "megumi.nakamura@example.com",
age: 35,
status: "inactive",
role: "user",
createdAt: "2023-08-20",
},
];
基本的なテーブルコンポーネントの作成
まずは最小限の構成でテーブルを表示してみましょう。src/components/BasicTable.tsxを作成します。
import {
useReactTable,
getCoreRowModel,
flexRender,
createColumnHelper,
} from "@tanstack/react-table";
import type { User } from "../data/users";
interface BasicTableProps {
data: User[];
}
// 列ヘルパーを作成(型安全な列定義のため)
const columnHelper = createColumnHelper<User>();
// 列定義
const columns = [
columnHelper.accessor("id", {
header: "ID",
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => `${row.lastName} ${row.firstName}`, {
id: "fullName",
header: "氏名",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "メールアドレス",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("age", {
header: "年齢",
cell: (info) => `${info.getValue()}歳`,
}),
columnHelper.accessor("status", {
header: "ステータス",
cell: (info) => {
const status = info.getValue();
const statusMap = {
active: "有効",
inactive: "無効",
pending: "保留中",
};
return statusMap[status];
},
}),
columnHelper.accessor("role", {
header: "権限",
cell: (info) => {
const role = info.getValue();
const roleMap = {
admin: "管理者",
user: "ユーザー",
guest: "ゲスト",
};
return roleMap[role];
},
}),
columnHelper.accessor("createdAt", {
header: "登録日",
cell: (info) => info.getValue(),
}),
];
function BasicTable({ data }: BasicTableProps) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="table-container">
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
export default BasicTable;
ポイント解説
createColumnHelper<User>(): 型安全な列定義を作成するためのヘルパーcolumnHelper.accessor(): データのプロパティにアクセスする列を定義useReactTable(): テーブルインスタンスを作成getCoreRowModel(): 基本的な行モデルを取得(必須)flexRender(): ヘッダーやセルをレンダリングするユーティリティ
スタイリング
src/App.cssを以下のように編集します。
(長いので折りたたみを開いて表示して下さい)
.App {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: #333;
margin-bottom: 1.5rem;
}
.table-container {
overflow-x: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
th,
td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #333;
white-space: nowrap;
}
tbody tr:hover {
background: #f8f9fa;
}
tbody tr:last-child td {
border-bottom: none;
}
/* ステータスバッジ */
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
/* 権限バッジ */
.role-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.role-admin {
background: #cce5ff;
color: #004085;
}
.role-user {
background: #e2e3e5;
color: #383d41;
}
.role-guest {
background: #f5f5f5;
color: #6c757d;
}
/* ソートボタン */
.sortable {
cursor: pointer;
user-select: none;
}
.sortable:hover {
background: #e9ecef;
}
.sort-indicator {
margin-left: 0.5rem;
}
/* グローバルフィルター */
.global-filter {
margin-bottom: 1rem;
}
.global-filter input {
padding: 0.75rem 1rem;
width: 100%;
max-width: 400px;
border: 1px solid #dee2e6;
border-radius: 8px;
font-size: 1rem;
}
.global-filter input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
/* ページネーション */
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.pagination-buttons {
display: flex;
gap: 0.5rem;
}
.pagination button {
padding: 0.5rem 1rem;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.pagination button:hover:not(:disabled) {
background: #0056b3;
}
.pagination button:disabled {
background: #ccc;
cursor: not-allowed;
}
.pagination-info {
color: #666;
font-size: 0.875rem;
}
.page-size-select {
padding: 0.5rem;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 0.875rem;
}
/* 行選択 */
.row-selected {
background: #e7f1ff !important;
}
.checkbox {
width: 1rem;
height: 1rem;
cursor: pointer;
}
/* アクションボタン */
.action-buttons {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.25rem 0.75rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 500;
}
.btn-edit {
background: #ffc107;
color: #212529;
}
.btn-edit:hover {
background: #e0a800;
}
.btn-delete {
background: #dc3545;
color: white;
}
.btn-delete:hover {
background: #c82333;
}
/* 選択されたアイテムの操作バー */
.selection-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
margin-bottom: 1rem;
background: #e7f1ff;
border-radius: 8px;
border: 1px solid #b6d4fe;
}
.selection-bar span {
color: #004085;
font-weight: 500;
}
.selection-bar button {
padding: 0.5rem 1rem;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.selection-bar button:hover {
background: #c82333;
}
src/index.cssも更新します。
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f5f5f5;
}
App.tsx の作成と動作確認
src/App.tsxを以下のように編集します。
import "./App.css";
import BasicTable from "./components/BasicTable";
import { users } from "./data/users";
function App() {
return (
<div className="App">
<h1>ユーザー管理テーブル</h1>
<BasicTable data={users} />
</div>
);
}
export default App;
npm run dev
ブラウザで http://localhost:5173 を開くと、基本的なテーブルが表示されます。

全機能統合版テーブルの作成
基本が理解できたところで、ソート・フィルタリング・ページネーション・行選択・列表示切替をすべて備えた完成版を作成しましょう。
src/components/FullFeaturedTable.tsxを作成します
import { useState, useMemo } from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
createColumnHelper,
type SortingState,
type ColumnFiltersState,
type PaginationState,
type RowSelectionState,
type VisibilityState,
type HeaderContext,
type CellContext,
} from "@tanstack/react-table";
import type { User } from "../data/users";
interface FullFeaturedTableProps {
data: User[];
onEdit?: (user: User) => void;
onDelete?: (user: User) => void;
}
const columnHelper = createColumnHelper<User>();
function FullFeaturedTable({ data, onEdit, onDelete }: FullFeaturedTableProps) {
// 状態管理
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState("");
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [showColumnMenu, setShowColumnMenu] = useState(false);
// 列定義
const columns = useMemo(
() => [
// 選択チェックボックス列
{
id: "select",
header: ({ table }: HeaderContext<User, unknown>) => (
<input
type="checkbox"
className="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }: CellContext<User, unknown>) => (
<input
type="checkbox"
className="checkbox"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
/>
),
enableSorting: false,
enableHiding: false,
},
columnHelper.accessor("id", {
header: "ID",
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => `${row.lastName} ${row.firstName}`, {
id: "fullName",
header: "氏名",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "メールアドレス",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("age", {
header: "年齢",
cell: (info) => `${info.getValue()}歳`,
}),
columnHelper.accessor("status", {
header: "ステータス",
cell: (info) => {
const status = info.getValue();
const statusMap = {
active: "有効",
inactive: "無効",
pending: "保留中",
};
const statusClass = `status-badge status-${status}`;
return <span className={statusClass}>{statusMap[status]}</span>;
},
filterFn: "equals",
}),
columnHelper.accessor("role", {
header: "権限",
cell: (info) => {
const role = info.getValue();
const roleMap = {
admin: "管理者",
user: "ユーザー",
guest: "ゲスト",
};
const roleClass = `role-badge role-${role}`;
return <span className={roleClass}>{roleMap[role]}</span>;
},
filterFn: "equals",
}),
columnHelper.accessor("createdAt", {
header: "登録日",
cell: (info) => info.getValue(),
}),
// アクション列
{
id: "actions",
header: "操作",
cell: ({ row }: CellContext<User, unknown>) => (
<div className="action-buttons">
<button
className="btn btn-edit"
onClick={() => onEdit?.(row.original)}
>
編集
</button>
<button
className="btn btn-delete"
onClick={() => onDelete?.(row.original)}
>
削除
</button>
</div>
),
enableSorting: false,
enableHiding: false,
},
],
[onEdit, onDelete]
);
const table = useReactTable({
data,
columns,
state: {
sorting,
columnFilters,
globalFilter,
pagination,
rowSelection,
columnVisibility,
},
enableRowSelection: true,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
const selectedRows = table.getFilteredSelectedRowModel().rows;
const handleBulkDelete = () => {
const selectedIds = selectedRows.map((row) => row.original.id);
if (window.confirm(`${selectedIds.length}件のユーザーを削除しますか?`)) {
alert(`削除対象ID: ${selectedIds.join(", ")}`);
setRowSelection({});
}
};
return (
<div>
{/* コントロールバー */}
<div
style={{
display: "flex",
gap: "1rem",
marginBottom: "1rem",
flexWrap: "wrap",
alignItems: "center",
}}
>
{/* グローバル検索 */}
<input
type="text"
value={globalFilter ?? ""}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="検索..."
style={{
padding: "0.5rem 1rem",
border: "1px solid #dee2e6",
borderRadius: "4px",
fontSize: "1rem",
minWidth: "200px",
}}
/>
{/* ステータスフィルター */}
<select
value={(table.getColumn("status")?.getFilterValue() as string) ?? ""}
onChange={(e) =>
table
.getColumn("status")
?.setFilterValue(e.target.value || undefined)
}
className="page-size-select"
>
<option value="">すべてのステータス</option>
<option value="active">有効</option>
<option value="inactive">無効</option>
<option value="pending">保留中</option>
</select>
{/* 権限フィルター */}
<select
value={(table.getColumn("role")?.getFilterValue() as string) ?? ""}
onChange={(e) =>
table.getColumn("role")?.setFilterValue(e.target.value || undefined)
}
className="page-size-select"
>
<option value="">すべての権限</option>
<option value="admin">管理者</option>
<option value="user">ユーザー</option>
<option value="guest">ゲスト</option>
</select>
{/* フィルタークリア */}
<button
onClick={() => {
setGlobalFilter("");
setColumnFilters([]);
}}
className="btn"
style={{ background: "#6c757d", color: "white" }}
>
クリア
</button>
{/* 列表示設定 */}
<div style={{ position: "relative", marginLeft: "auto" }}>
<button
onClick={() => setShowColumnMenu(!showColumnMenu)}
className="btn"
style={{ background: "#007bff", color: "white" }}
>
列の表示 ▼
</button>
{showColumnMenu && (
<div
style={{
position: "absolute",
top: "100%",
right: 0,
marginTop: "0.5rem",
padding: "1rem",
background: "white",
border: "1px solid #dee2e6",
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
zIndex: 100,
minWidth: "180px",
}}
>
{table
.getAllLeafColumns()
.filter((column) => column.getCanHide())
.map((column) => (
<label
key={column.id}
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.25rem",
}}
>
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/>
{typeof column.columnDef.header === "string"
? column.columnDef.header
: column.id}
</label>
))}
<button
onClick={() => setShowColumnMenu(false)}
className="btn"
style={{
marginTop: "0.5rem",
background: "#6c757d",
color: "white",
width: "100%",
}}
>
閉じる
</button>
</div>
)}
</div>
</div>
{/* 選択時の一括操作バー */}
{selectedRows.length > 0 && (
<div className="selection-bar">
<span>{selectedRows.length} 件選択中</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button onClick={handleBulkDelete}>一括削除</button>
<button
onClick={() => setRowSelection({})}
style={{ background: "#6c757d" }}
>
選択解除
</button>
</div>
</div>
)}
{/* テーブル */}
<div className="table-container">
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className={header.column.getCanSort() ? "sortable" : ""}
onClick={
header.column.getCanSort()
? header.column.getToggleSortingHandler()
: undefined
}
>
<div style={{ display: "flex", alignItems: "center" }}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && (
<span className="sort-indicator">
{{
asc: " 🔼",
desc: " 🔽",
}[header.column.getIsSorted() as string] ?? " ↕️"}
</span>
)}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.length === 0 ? (
<tr>
<td
colSpan={table.getVisibleLeafColumns().length}
style={{ textAlign: "center", padding: "2rem" }}
>
該当するデータがありません
</td>
</tr>
) : (
table.getRowModel().rows.map((row) => (
<tr
key={row.id}
className={row.getIsSelected() ? "row-selected" : ""}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
{/* ページネーション */}
<div className="pagination">
<div className="pagination-buttons">
<button
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
{"<<"}
</button>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{"<"}
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{">"}
</button>
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{">>"}
</button>
</div>
<div className="pagination-info">
ページ {table.getState().pagination.pageIndex + 1} /{" "}
{table.getPageCount() || 1}
(全 {table.getFilteredRowModel().rows.length} 件)
</div>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))}
className="page-size-select"
>
{[5, 10, 20, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}件表示
</option>
))}
</select>
</div>
</div>
);
}
export default FullFeaturedTable;
App.tsx を更新して完成版を表示
src/App.tsxを更新します
import "./App.css";
import FullFeaturedTable from "./components/FullFeaturedTable";
import { users, type User } from "./data/users";
function App() {
const handleEdit = (user: User) => {
alert(`編集: ${user.lastName} ${user.firstName} (ID: ${user.id})`);
};
const handleDelete = (user: User) => {
if (window.confirm(`${user.lastName} ${user.firstName} を削除しますか?`)) {
alert(`削除: ID ${user.id}`);
}
};
return (
<div className="App">
<h1>ユーザー管理テーブル</h1>
<FullFeaturedTable
data={users}
onEdit={handleEdit}
onDelete={handleDelete}
/>
</div>
);
}
export default App;
これで、以下の機能を持つテーブルが完成しました。
- ソート(列ヘッダークリック)
- グローバル検索
- 列フィルター(ステータス・権限)
- ページネーション
- 行選択と一括操作
- 列の表示/非表示切替
- 編集・削除アクション
実際に起動した画面は以下の通りです。

機能追加のポイント解説
各機能を追加する際の重要なポイントをまとめます。
- ソート機能
import { getSortedRowModel, SortingState } from "@tanstack/react-table";
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
// ...
state: { sorting },
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(), // これを追加
});
- フィルタリング機能
import { getFilteredRowModel, ColumnFiltersState } from "@tanstack/react-table";
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState("");
const table = useReactTable({
// ...
state: { columnFilters, globalFilter },
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
getFilteredRowModel: getFilteredRowModel(), // これを追加
});
- ページネーション機能
import { getPaginationRowModel, PaginationState } from "@tanstack/react-table";
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const table = useReactTable({
// ...
state: { pagination },
onPaginationChange: setPagination,
getPaginationRowModel: getPaginationRowModel(), // これを追加
});
- 行選択機能
import { RowSelectionState } from "@tanstack/react-table";
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const table = useReactTable({
// ...
state: { rowSelection },
enableRowSelection: true, // これを追加
onRowSelectionChange: setRowSelection,
});
まとめ
TanStack Table は、モダンな Web アプリケーションで複雑なテーブル UI を構築するための強力なライブラリです。
TanStack Table を使うメリットとしては、主に以下があります。
| メリット | 説明 |
|---|---|
| 完全なカスタマイズ性 | ヘッドレスなので、どんなデザインにも対応可能 |
| 豊富な機能 | ソート、フィルタリング、ページネーション、行選択など |
| 型安全性 | TypeScript との親和性が高い |
| パフォーマンス | 必要な機能だけをインポート |
| フレームワーク非依存 | React、Vue、Solid、Svelte で使用可能 |
こんな場合におすすめです。
- 複雑なテーブル UI が必要なアプリケーション
- デザインの自由度が求められるプロジェクト
- TypeScript で型安全に開発したい場合
- 大量のデータを効率的に表示したい場合
ぜひ実際に手を動かして、TanStack Table の便利さを体感してください。
完成したプロジェクト構造
tanstack-table-demo/
├── src/
│ ├── components/
│ │ ├── BasicTable.tsx
│ │ └── FullFeaturedTable.tsx
│ ├── data/
│ │ └── users.ts
│ ├── App.tsx
│ ├── App.css
│ ├── main.tsx
│ └── index.css
├── package.json
└── vite.config.ts