TanStack Table v8 実装ガイド
TanStack Table v8(headless table)で柔軟なテーブル UI を組むパターン。最小実装、行選択 / リサイズの実装までを 1 ページに統合。
TanStack Table v8 で headless にテーブルを組む
TanStack Table v8(@tanstack/react-table)で sort / filter / pagination を実装する最小コード、core / sorted / filtered Row Model の組み合わせ方を触れる demo で確認する実装メモ。
検証日: 2026-05-09
使用バージョン:
@tanstack/react-table@8.x対象: React + TypeScript でデータテーブルが必要、自前 UI を当てたい人
TanStack Table v8 を headless(state とロジックだけ提供、UI は呼び出し側で組む)で扱う基本パターン。ColumnDef<T> の型付き定義、sort / filter / pagination の組み合わせ、Row Model の概念を動く demo で確認します。
触って試す
| ID | Name | Role | Commits |
|---|---|---|---|
| 1 | Alice | frontend | 142 |
| 2 | Bob | backend | 88 |
| 3 | Charlie | devops | 211 |
| 4 | Dana | frontend | 65 |
| 5 | Erin | data | 197 |
| 6 | Frank | backend | 34 |
ヘッダクリックで sort、上のテキストボックスで全列フィルタ。
なぜ headless か
TanStack Table は UI を提供しない。代わりに「state(sort 状態 / filter 値 / page index)を管理し、表示すべき rows の配列を返す」だけ。スタイルは Tailwind / CSS で自前。
メリット:
<table>の構造を完全に自分で書ける(意味的 HTML、a11y、印刷スタイル等)- デザインシステム(shadcn / MUI / 自社)との衝突がない
- bundle サイズが小さい(15KB 前後 min+gzip)
代わりに、ボイラープレートは多め。
最小サンプル
ColumnDef<T> 配列 + useReactTable から table.getRowModel() を取って、テーブル DOM を組む最小コード:
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
type User = { id: number; name: string; commits: number };
const data: User[] = [
{ id: 1, name: "Alice", commits: 142 },
{ id: 2, name: "Bob", commits: 88 },
];
const columns: ColumnDef<User>[] = [
{ accessorKey: "id", header: "ID" },
{ accessorKey: "name", header: "Name" },
{ accessorKey: "commits", header: "Commits" },
];
export function Table() {
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
return (
<table>
<thead>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((h) => (
<th key={h.id}>{flexRender(h.column.columnDef.header, h.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>
);
}
ソートを足す
sorting state を useState で持ち、getSortedRowModel() を有効化 + <th onClick={header.column.getToggleSortingHandler()}> で列クリックで sort:
import { getSortedRowModel, SortingState } from "@tanstack/react-table";
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
data, columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), // ← 追加
});
// header 描画で
<th onClick={h.column.getToggleSortingHandler()}>
{flexRender(h.column.columnDef.header, h.getContext())}
{h.column.getIsSorted() === "asc" ? " ▲" : h.column.getIsSorted() === "desc" ? " ▼" : ""}
</th>
グローバルフィルタ
検索ボックスで全列を横断する filter。globalFilter state + getFilteredRowModel() を組み合わせる:
import { getFilteredRowModel } from "@tanstack/react-table";
const [filter, setFilter] = useState("");
const table = useReactTable({
data, columns,
state: { sorting, globalFilter: filter },
onGlobalFilterChange: setFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
ページネーション
getPaginationRowModel() を有効化。table.previousPage() / nextPage() でページ送り、getCanPreviousPage / getCanNextPage で先頭・末尾判定:
import { getPaginationRowModel } from "@tanstack/react-table";
const table = useReactTable({
// ...
initialState: { pagination: { pageSize: 10 } },
getPaginationRowModel: getPaginationRowModel(),
});
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>前</button>
<span>{table.getState().pagination.pageIndex + 1} / {table.getPageCount()}</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>次</button>
つまずいたポイント
get*RowModel()を register しないと機能しない:getSortedRowModelを渡し忘れると sort UI が動かないflexRenderを忘れると JSX が出ない:cell / header 描画は必ず flexRender 経由accessorKeyとaccessorFnの使い分け:ネスト値や computed は fn で取り出す- sort indicator は自前:▲ ▼ や
aria-sortは自分で書く必要あり - virtualization は別ライブラリ(
@tanstack/react-virtualを組み合わせ)。1 万行超は virtuoso 系を検討
評価
| 観点 | 評価 | コメント |
|---|---|---|
| 学習コスト | ○ | 概念(Row Model)が独特、最初の 1 つを書くまでは時間 |
| 型サポート | ◎ | ColumnDef<T> で全 cell が型推論 |
| カスタマイズ性 | ◎ | 完全 headless、UI を制約しない |
| 機能 | ◎ | sort / filter / pagination / グルーピング / expand / row selection — column resize や行範囲選択は /articles/tanstack-table-v8-resize-selection/ |
| バンドル | ○ | 15KB 前後 |
向く / 向かないケース
- 向く: デザインシステムが固まっているプロジェクト、admin パネル、ダッシュボード、CRUD 画面
- 向かない: 「数行コードで動くテーブル UI が欲しい」(MUI X DataGrid / AG Grid のような複合 UI 製品)
- 向かない: 1 万行超の virtual scroll(react-virtuoso との合成 or別ライブラリ)
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…
Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門
FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発
新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…
【POD】React & Gatsby開発入門
TanStack Table v8 で列幅 + 列表示 + 行選択 + ページング
TanStack Table v8 の踏み込んだ機能 — column resize / column visibility / row selection / pagination を 1 つのテーブルで組み合わせる実装パターンを触れる demo で確認する実装メモ。
検証日: 2026-05-10
使用バージョン:
@tanstack/react-table@8.x対象: 入門は通っている、admin / dashboard 級のテーブル UI を組みたい人
TanStack Table v8 で column resize / row selection(範囲含む) を組むパターン。enableColumnResizing + columnResizeMode、rowSelection state、shift-click による範囲選択、を動く demo で確認します。
触って試す
| ID | Name | Role | Commits | Reviews | Updated | |
|---|---|---|---|---|---|---|
| 1 | Alice | frontend | 8 | 4 | 2026-05-19 | |
| 2 | Bob | backend | 39 | 21 | 2026-05-16 | |
| 3 | Charlie | devops | 70 | 38 | 2026-05-13 | |
| 4 | Dana | data | 101 | 55 | 2026-05-10 | |
| 5 | Erin | design | 132 | 72 | 2026-05-07 | |
| 6 | Frank | frontend | 163 | 9 | 2026-05-04 | |
| 7 | Grace | backend | 194 | 26 | 2026-05-01 | |
| 8 | Alice | devops | 225 | 43 | 2026-04-28 |
ヘッダ右端の縦線を ドラッグで列幅変更、上のチェックボックスで 列表示切替、行のチェックボックスで 選択、下のボタンで ページング。
機能を 1 つの table に同居させる
TanStack Table の機能はそれぞれ:
- state 管理(
useState) - state を
useReactTableのstateに渡す - 対応する
on*Changeハンドラを渡す - 対応する
get*RowModelを register - JSX で対応する getter を呼ぶ
の 5 ステップ揃えて初めて動く。最小化したサンプル:
import {
type ColumnDef,
type SortingState,
type RowSelectionState,
type VisibilityState,
type ColumnSizingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
function FullFeatureTable({ data }: { data: Row[] }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [filter, setFilter] = useState("");
const [colSize, setColSize] = useState<ColumnSizingState>({});
const [colVis, setColVis] = useState<VisibilityState>({});
const [rowSel, setRowSel] = useState<RowSelectionState>({});
const table = useReactTable({
data,
columns,
state: { sorting, globalFilter: filter, columnSizing: colSize, columnVisibility: colVis, rowSelection: rowSel },
onSortingChange: setSorting,
onGlobalFilterChange: setFilter,
onColumnSizingChange: setColSize,
onColumnVisibilityChange: setColVis,
onRowSelectionChange: setRowSel,
columnResizeMode: "onChange",
enableRowSelection: true,
initialState: { pagination: { pageSize: 8 } },
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
// ... render
}
1. Column Resize(列幅ドラッグ)
enableColumnResizing を有効にして <th> の右端に drag ハンドル を置き、header.getResizeHandler() を mouse/touch event に bind:
const table = useReactTable({
...,
columnResizeMode: "onChange", // または "onEnd"
});
// header 描画
<th key={h.id} style={{ width: h.getSize(), position: "relative" }}>
{flexRender(h.column.columnDef.header, h.getContext())}
{h.column.getCanResize() && (
<span
onMouseDown={h.getResizeHandler()}
onTouchStart={h.getResizeHandler()}
style={{
position: "absolute", right: 0, top: 0, height: "100%",
width: 6, cursor: "col-resize",
background: h.column.getIsResizing() ? "blue" : "transparent",
}}
/>
)}
</th>
ポイント:
columnResizeMode: "onChange"= ドラッグ中もリアルタイム反映、"onEnd"= ドラッグ終了時に確定<th>にposition: relative、resize handle はposition: absoluteで右端- handle width は 6px 程度、touch にも反応するよう
onTouchStartも繋ぐ - 特定列を不可にする:
enableResizing: false(チェックボックス列など)
2. Column Visibility(列の表示切替)
columnVisibility state を useState で持ち、各列の表示 / 非表示を toggle するチェックボックスを描く:
{table.getAllLeafColumns().map((col) => (
<label key={col.id}>
<input
type="checkbox"
checked={col.getIsVisible()}
onChange={col.getToggleVisibilityHandler()}
/>
{String(col.columnDef.header)}
</label>
))}
getAllLeafColumns() で全列を列挙。getToggleVisibilityHandler() で onChange handler を取得して input に渡す。
3. Row Selection(チェックボックスで行選択)
rowSelection state を有効化し、ヘッダ行に「全選択」、各行にチェックボックスを置く。getIsAllPageRowsSelected / getIsSomePageRowsSelected で indeterminate 状態も判定:
// 「全選択」のヘッダセル
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
// 各行のセル
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
選択数の取得:
const selectedCount = Object.values(rowSel).filter(Boolean).length;
選択された Row の data を一括取得:
const selectedRows = table.getSelectedRowModel().rows.map((r) => r.original);
4. Pagination
getPaginationRowModel() を有効化し、table.setPageSize と table.previousPage / nextPage でページ操作を組む:
const table = useReactTable({
...,
initialState: { pagination: { pageSize: 8 } },
getPaginationRowModel: getPaginationRowModel(),
});
// JSX
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>前</button>
<span>{table.getState().pagination.pageIndex + 1} / {table.getPageCount()}</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>次</button>
サーバー側ページングしたい場合は manualPagination: true + pageCount を渡す。
5. ColumnDef でセルのカスタムレンダリング
columns の cell プロパティに JSX を返す関数を渡せば、セル単位で表示を組み替えできる(アイコン / リンク / バッジ等):
const columns: ColumnDef<Row>[] = [
{ accessorKey: "id", header: "ID", size: 60 },
{
accessorKey: "status",
header: "Status",
cell: ({ getValue }) => {
const v = getValue<string>();
return <span className={v === "ok" ? "text-green-600" : "text-red-600"}>{v}</span>;
},
},
{
id: "actions",
header: "",
cell: ({ row }) => <button onClick={() => del(row.original.id)}>削除</button>,
enableResizing: false,
enableSorting: false,
},
];
accessorKey で値直結、cell で表示カスタマイズ、accessorFn で計算列も書ける。
つまずいたポイント
<table>のwidthはgetCenterTotalSize()で動的指定:列幅を合計した width を table 全体に設定しないと、resize 後に拡縮しないoverflow: autoを親 div に:列を広げると table が親より大きくなり、横スクロールが必要select列はenableResizing: false+enableSorting: falseで誤操作防止onRowSelectionChangeの値型はRecord<string, boolean>:row.id ではなく row index を key にした boolean マップgetRowIdを渡すと選択状態が永続化しやすい:getRowId: (row) => String(row.id)で安定 ID- virtual scroll と組み合わせる時は
@tanstack/react-virtualを別途追加(react-virtuoso よりこちらが Table と相性よい)
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…
Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門
FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発
新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…