TanStack Table v9 実装ガイド(v8 からの移行つき)
TanStack Table v9(headless table)で柔軟なテーブル UI を組むパターン。useReactTable から useTable への書き換え、バンドルサイズの実測、列幅 / 列表示 / 行選択、「全選択」が選ぶ行数の実測までを 1 ページに統合。
TanStack Table v9 で headless にテーブルを組む(v8 からの書き換えつき)
TanStack Table v9(@tanstack/react-table 9.2)で sort / filter / pagination を組む最小コード。useReactTable から useTable への書き換え対応表、機能の登録数ごとのバンドルサイズ実測、並べ替え関数の登録漏れで順序が変わる落とし穴を、触れる demo で確認する。
検証日: 2026-09-19(v9 で全面書き直し。初版 2026-05-09 は v8)
使用バージョン:
@tanstack/react-table@9.2.4/ React 19対象: React + TypeScript でデータテーブルが必要、自前 UI を当てたい人。v8 のコードを v9 に上げる人
TanStack Table を headless(state とロジックだけ提供、UI は呼び出し側で組む)で扱う基本パターンを v9 で書き直しました。v9 は「使う機能を自分で登録する」作りに変わり、v8 の useReactTable + getCoreRowModel() の形はそのままでは動きません。このページの demo とコードはすべて 9.2.4 で型チェックと動作確認を通したものです。
触って試す
| 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、上のテキストボックスで全列フィルタ。
この実装を体系立てて学ぶ本(ライブラリの API を追う前に、React のレンダリングと状態の考え方を固めると設計で迷わない)
改訂新版 これからはじめるReact実践入門 : コンポーネントの基本からNext.jsによるアプリ開発まで
React 19 / Next.js 15 対応の基礎から本格アプリ開発を体系的に習得する
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
React Hooks ベースで Web アプリを手を動かしながら実装する
v8 から何が変わったか
公式の 移行ガイド から、この記事の範囲で効くものだけ抜き出します。
| v8 | v9 |
|---|---|
useReactTable({...}) | useTable({ features, ... }) |
| 全機能が最初から入っている | tableFeatures({...}) に使う機能を登録する。全部入りは stockFeatures |
getCoreRowModel: getCoreRowModel() | 不要(core は常に有効) |
getSortedRowModel: getSortedRowModel() | tableFeatures の中に sortedRowModel: createSortedRowModel() |
getFilteredRowModel() / getPaginationRowModel() | filteredRowModel: createFilteredRowModel() / paginatedRowModel: createPaginatedRowModel() |
| 並べ替え・絞り込み関数は組み込み済み | sortFns / filterFns に使う関数を登録(sortFn_alphanumeric など個別 import) |
ColumnDef<User> | ColumnDef<typeof features, User>(機能の型が第 1 引数) |
table.getState().pagination | table.state.pagination |
flexRender(h.column.columnDef.header, h.getContext()) | <table.FlexRender header={h} />(flexRender も残っている) |
const { getValue } = row | row.getValue(...)(メソッドが prototype に移り、分割代入すると this を失う) |
急ぎで上げるだけなら @tanstack/react-table/legacy の useLegacyTable が v8 の書き方のまま動きます。ただし公式が「一時的な移行用・非推奨」と明記していて、下の実測のとおりバンドルは v8 より大きくなります。
最小サンプル
tableFeatures({}) に何も登録しなければ、表示だけのテーブルになります:
import { type ColumnDef, tableFeatures, useTable } from "@tanstack/react-table";
type User = { id: number; name: string; commits: number };
const features = tableFeatures({}); // コンポーネントの外で 1 回だけ作る
const data: User[] = [
{ id: 1, name: "Alice", commits: 142 },
{ id: 2, name: "Bob", commits: 88 },
];
const columns: ColumnDef<typeof features, User>[] = [
{ accessorKey: "id", header: "ID" },
{ accessorKey: "name", header: "Name" },
{ accessorKey: "commits", header: "Commits" },
];
export function Table() {
const table = useTable({ features, data, columns });
return (
<table>
<thead>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((h) => (
<th key={h.id}><table.FlexRender header={h} /></th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getAllCells().map((cell) => (
<td key={cell.id}><table.FlexRender cell={cell} /></td>
))}
</tr>
))}
</tbody>
</table>
);
}
v8 で書いていた row.getVisibleCells() は、v9 では columnVisibilityFeature を登録したときだけ型に現れます。列の表示切替を使わないテーブルでは row.getAllCells() を使います(登録せずに getVisibleCells を呼ぶと TypeScript が「そのプロパティは無い」と止めてくれます)。
ソートを足す
機能(rowSortingFeature)、row model(sortedRowModel)、比較関数(sortFns)の 3 つを同じ tableFeatures に入れます:
import {
type SortingState,
createSortedRowModel,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_basic,
tableFeatures,
useTable,
} from "@tanstack/react-table";
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
});
const [sorting, setSorting] = useState<SortingState>([]);
const table = useTable({
features, data, columns,
state: { sorting },
onSortingChange: setSorting,
});
// header 描画で
<th onClick={h.column.getToggleSortingHandler()}>
<table.FlexRender header={h} />
{h.column.getIsSorted() === "asc" ? " ▲" : h.column.getIsSorted() === "desc" ? " ▼" : ""}
</th>
並べ替え関数の登録漏れは、エラーにならず順序だけが変わる
v9 では列の sortFn を省略すると値を見て関数名が自動で選ばれますが、その名前が sortFns に登録されていないとエラーは出ず、単純比較に落ちます。name 列に item9 と item10 を入れて昇順にした結果を 9.2.4 で確かめました:
登録した sortFns | name 昇順の結果 |
|---|---|
alphanumeric + basic + text | Alice, Bob, Charlie, item9, item10 |
basic だけ | Alice, Bob, Charlie, item10, item9 |
数字まじりの文字列(ファイル名・型番・連番つきの名前)を並べる列があるなら、sortFn_alphanumeric を登録しておきます。数値だけの列は basic で足ります。公式ガイドは全関数入りの sortFns / filterFns をまるごと渡す書き方を非推奨にしていて、個別 import が推奨です。
グローバルフィルタ
検索ボックスで全列を横断する filter。globalFilteringFeature は columnFilteringFeature が前提で、片方だけ登録すると tableFeatures の型エラーになります(実行前に気づけます):
import {
columnFilteringFeature,
createFilteredRowModel,
filterFn_includesString,
globalFilteringFeature,
} from "@tanstack/react-table";
const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
globalFilteringFeature,
sortedRowModel: createSortedRowModel(),
filteredRowModel: createFilteredRowModel(),
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
filterFns: { includesString: filterFn_includesString },
});
const [filter, setFilter] = useState("");
const table = useTable({
features, data, columns,
state: { sorting, globalFilter: filter },
onSortingChange: setSorting,
onGlobalFilterChange: setFilter,
});
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
ページネーション
rowPaginationFeature と paginatedRowModel を登録。ページ番号は table.getState() ではなく table.state から読みます:
import { createPaginatedRowModel, rowPaginationFeature } from "@tanstack/react-table";
const features = tableFeatures({
// ...
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
});
const table = useTable({
// ...
initialState: { pagination: { pageIndex: 0, pageSize: 10 } },
});
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>前</button>
<span>{table.state.pagination.pageIndex + 1} / {table.getPageCount()}</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>次</button>
initialState.pagination は pageIndex と pageSize の両方が型で必須です(v8 は pageSize だけで通りました)。
機能を絞るとどれだけ小さくなるか(実測)
v9 の売りは「使う機能だけをバンドルに入れる」ことです。同じ条件(esbuild 0.27.7 で --bundle --minify、react は external、gzip -9)で、テーブルを 1 つ作るだけのファイルを束ねて比べました。
上げれば軽くなると思って測り始めたのですが、よく使う組み合わせでは逆の結果でした。読み取れること:
- 表示だけのテーブルは v9 が小さい(v8 の core より約 2.3 KB 減)
- sort + filter + pagination まで入れると v9 のほうが約 1.6 KB 大きい。状態管理が TanStack Store に載った分が乗っています。「v9 に上げれば軽くなる」とは限りません
stockFeaturesは v8 の約 2 倍。v8 の感覚で全部入りにすると重くなるので、機能は自分で選ぶ前提です- 関数の個別登録で減るのは 0.2 KB 程度。サイズ目的というより、上の「登録漏れで順序が変わる」を意識して書くための作法と捉えたほうが実態に合います
Vite(Rollup)でも傾向は同じはずですが、数値はバンドラで変わります。自分のアプリでは vite build の出力で確かめてください。
つまずいたポイント
featuresはコンポーネントの外で作る:tableFeatures({...})を描画のたびに作り直すと、テーブルが毎回作り直しになる- 機能を登録していない API は型に出てこない:
getVisibleCells(列表示)やgetIsSelected(行選択)が「無い」と言われたら、機能の登録漏れ globalFilteringFeature単体は型エラー:columnFilteringFeatureも一緒に登録する- 並べ替え関数の登録漏れは黙って順序が変わる:上の節のとおり。テストで
item9/item10のような値を 1 つ入れておくと気づける - 行やセルのメソッドを分割代入しない:
const { getValue } = rowは v9 で壊れる。row.getValue("name")と書く - sort indicator は自前:▲ ▼ や
aria-sortは自分で書く(v8 から変わらず) - virtualization は別ライブラリ(
@tanstack/react-virtualを組み合わせ)。1 万行超は virtuoso 系を検討
評価
| 観点 | 評価 | コメント |
|---|---|---|
| 学習コスト | △ | Row Model に加えて「機能の登録」が増えた。最初の 1 つを書くまでの時間は v8 より長い |
| 型サポート | ◎ | 登録した機能だけが型に現れる。登録漏れ・前提機能の不足がコンパイル時に分かる |
| カスタマイズ性 | ◎ | 完全 headless、UI を制約しない |
| 機能 | ◎ | sort / filter / pagination / グルーピング / expand / row selection — 列幅・行選択・全選択は 列幅・行選択編 |
| バンドル | ○ | 表示だけなら 10.5 KB、sort + filter + pagination で 15.6 KB(上の実測) |
向く / 向かないケース
- 向く: デザインシステムが固まっているプロジェクト、admin パネル、ダッシュボード、CRUD 画面
- 向かない: 「数行コードで動くテーブル UI が欲しい」(MUI X DataGrid / AG Grid のような複合 UI 製品)
- 向かない: 1 万行超の virtual scroll(react-virtuoso との合成 or 別ライブラリ)
- v8 のままでよい場合: 動いている v8 のテーブルで、バンドルにも型にも困っていないなら急いで上げる理由は薄い。新規のテーブルから v9 で書くのが無理のない順序
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
改訂新版 これからはじめるReact実践入門 : コンポーネントの基本からNext.jsによるアプリ開発まで
React 19 / Next.js 15 対応の基礎から本格アプリ開発を体系的に習得する
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
React Hooks ベースで Web アプリを手を動かしながら実装する
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発
『TypeScript と React/Next.js でつくる実践 Web アプリケーション開発』(2022 年 7 月、技術評論社)は
【POD】React & Gatsby開発入門
Gatsby でサイトを作りながら React のマークアップを習得する
TanStack Table v9 で列幅 + 列表示 + 行選択(全選択)+ ページング
TanStack Table v9(9.2)で column resize / column visibility / row selection / pagination を 1 つのテーブルに同居させる。「全選択」が現在ページ・絞り込み結果・全データのどれを指すかを 1,000 行の demo で数えて確かめ、v9 で変わった中間状態と Shift 範囲選択も扱う。
検証日: 2026-09-19(v9 で全面書き直し。初版 2026-05-10 は v8)
使用バージョン:
@tanstack/react-table@9.2.4/ React 19対象: 入門は通っている、admin / dashboard 級のテーブル UI を組みたい人
TanStack Table v9 で column resize / column visibility / row selection / pagination を 1 つのテーブルに同居させるパターン。後半で「全選択ボタンは何行を選ぶのか」を 1,000 行の demo で数えます。基本の書き方と v8 からの対応表は headless 編 にあります。
触って試す
| ID | Name | Role | Commits | Reviews | Updated | |
|---|---|---|---|---|---|---|
| 1 | Alice | frontend | 8 | 4 | 2026-09-23 | |
| 2 | Bob | backend | 39 | 21 | 2026-09-20 | |
| 3 | Charlie | devops | 70 | 38 | 2026-09-17 | |
| 4 | Dana | data | 101 | 55 | 2026-09-14 | |
| 5 | Erin | design | 132 | 72 | 2026-09-11 | |
| 6 | Frank | frontend | 163 | 9 | 2026-09-08 | |
| 7 | Grace | backend | 194 | 26 | 2026-09-05 | |
| 8 | Alice | devops | 225 | 43 | 2026-09-02 |
ヘッダ右端の縦線を ドラッグで列幅変更、上のチェックボックスで 列表示切替、行のチェックボックスで 選択、下のボタンで ページング。
この実装を体系立てて学ぶ本(機能を足すほど state が増える。React の状態の持ち方を先に固めておくと、どこに何を置くかで迷わない)
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
React Hooks ベースで Web アプリを手を動かしながら実装する
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発
『TypeScript と React/Next.js でつくる実践 Web アプリケーション開発』(2022 年 7 月、技術評論社)は
機能を 1 つの table に同居させる
v9 では機能ごとに次の 4 つを揃えます:
tableFeaturesに機能を登録(必要なら row model と関数も)- state を
useStateで持ち、useTableのstateに渡す - 対応する
on*Changeを渡す - JSX で対応する getter を呼ぶ
import {
type ColumnDef,
type ColumnSizingState,
type ColumnVisibilityState, // v8 の VisibilityState から改名
type RowSelectionState,
type SortingState,
columnFilteringFeature,
columnResizingFeature,
columnSizingFeature,
columnVisibilityFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
globalFilteringFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_basic,
tableFeatures,
useTable,
} from "@tanstack/react-table";
const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature, // globalFilteringFeature の前提
globalFilteringFeature,
rowPaginationFeature,
rowSelectionFeature,
columnSizingFeature, // 列が幅を持つ
columnResizingFeature, // ドラッグで幅を変える
columnVisibilityFeature,
sortedRowModel: createSortedRowModel(),
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
filterFns: { includesString: filterFn_includesString },
});
function FullFeatureTable({ data }: { data: Row[] }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [filter, setFilter] = useState("");
const [colSize, setColSize] = useState<ColumnSizingState>({});
const [colVis, setColVis] = useState<ColumnVisibilityState>({});
const [rowSel, setRowSel] = useState<RowSelectionState>({});
const table = useTable({
features,
data,
columns, // ColumnDef<typeof features, Row>[]
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: { pageIndex: 0, pageSize: 8 } },
});
// ... render
}
1. Column Resize(列幅ドラッグ)
v8 では 1 つだった機能が、v9 で columnSizingFeature(列が幅を持つ)と columnResizingFeature(ドラッグで変える) に分かれました。固定幅だけなら前者だけで足ります。ドラッグ中の状態は columnSizingInfo から columnResizing に改名されています。
<th> の右端に drag ハンドルを置き、header.getResizeHandler() を mouse / touch に bind するところは v8 と同じです:
<th key={h.id} style={{ width: h.getSize(), position: "relative" }}>
<table.FlexRender header={h} />
{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(列の表示切替)
columnVisibilityFeature を登録すると、row.getVisibleCells() と column.getToggleVisibilityHandler() が型に現れます:
{table.getAllLeafColumns().map((col) => (
<label key={col.id}>
<input
type="checkbox"
checked={col.getIsVisible()}
onChange={col.getToggleVisibilityHandler()}
/>
{String(col.columnDef.header)}
</label>
))}
行の描画は row.getVisibleCells() に切り替えます(getAllCells() のままだと、非表示にした列のセルが出続けます)。
3. Row Selection(チェックボックスで行選択)
// 各行のセル
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
選択された行のデータを取り出す:
const selectedRows = table.getSelectedRowModel().rows.map((r) => r.original);
「全選択」は 3 種類ある
「全選択」と言っても、TanStack Table の API が選ぶ範囲は呼び方で変わります。1,000 行のデータを role = design で 120 行に絞り込み、1 ページ 25 行で表示した状態で、ボタンごとに選択行数を数えました。下の demo で同じ操作を試せます。
| やりたいこと | 呼ぶもの | ヘッダのチェック状態 |
|---|---|---|
| いま見えているページだけ | table.toggleAllPageRowsSelected(true) / getToggleAllPageRowsSelectedHandler() | getIsAllPageRowsSelected() |
| 絞り込み結果のすべて(全ページ) | table.toggleAllRowsSelected(true) / getToggleAllRowsSelectedHandler() | getIsAllRowsSelected() |
| 絞り込みに関係なく全データ | setRowSelection(Object.fromEntries(data.map((d) => [String(d.id), true]))) | 自前で判定 |
名前から toggleAllRowsSelected は「全データ」に見えますが、対象は 絞り込み後の行 です。Gmail の「このページの 25 件を選択 → 検索結果の 120 件すべてを選択」のような 2 段階の UI は、① と ② の組み合わせでそのまま作れます。
注意したいのは選択の 持ち越し です。選択は行 id の集合として state に残るので、絞り込みを変えても消えません。実測では、③ で全データを選ぶと画面には 120 行しか見えていないのに table.getSelectedRowModel().rows.length は 1,000 を返しました(getFilteredSelectedRowModel() は 120)。逆に、② で 120 行を選んでから絞り込みを外すと、1,000 行のうち 120 行が選択されたまま残ります。この状態で「選択した行を削除」を getSelectedRowModel() で実装すると、利用者に見えていない 880 行まで消えます。
削除や一括更新に渡す行は、getSelectedRowModel() ではなく table.getFilteredSelectedRowModel()(いまの絞り込みに残っていて、かつ選択されている行)から取るのが安全です。絞り込みを変えたら table.resetRowSelection() で選択を捨てる、という割り切りも実務ではよく使います。
選択した行だけを xlsx に出す
「選んだ行をエクスポート」は行選択とセットで頼まれる機能です。demo の 2 つのボタンは、同じ選択状態から どの row model で行を取るか だけを変えて ExcelJS に渡します。③ で 1,000 行を選んだまま(画面は 120 行に絞り込み中)それぞれ押した結果:
const exportSelected = async () => {
// 画面の絞り込みに残っている選択行だけを出す
const rows = table.getFilteredSelectedRowModel().rows;
const ExcelJS = (await import("exceljs")).default; // 押されたときだけ読み込む
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("selected");
ws.columns = [
{ header: "ID", key: "id", width: 8 },
{ header: "Name", key: "name", width: 18 },
{ header: "Role", key: "role", width: 12 },
];
ws.getRow(1).font = { bold: true };
for (const r of rows) ws.addRow(r.original);
const buf = await wb.xlsx.writeBuffer();
const blob = new Blob([buf], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
// blob を <a download> に渡す
};
1,000 行でも 19 ms なので、速度を理由に出力範囲を絞る必要はありません。決めるべきは「利用者が出したつもりの行」と一致させることで、絞り込み中なら getFilteredSelectedRowModel()、出力前に「120 行を出力します」と行数を見せておくと食い違いに気づけます。exceljs は大きいライブラリなので、import() でボタンが押されたときだけ読み込むと、テーブルの初期表示に影響しません。
中間状態(indeterminate)は v9 で書き方が変わった
v8 の getIsSomeRowsSelected() は「一部だけ選択」のとき true でしたが、v9 では 「1 行でも選択されていれば true」 に変わり、全選択しても true のままです(移行ガイド に minor breaking change として記載)。v8 のコードをそのまま持ってくると、全選択したのにヘッダが「−」表示のままになります。
const all = table.getIsAllPageRowsSelected();
const some = table.getIsSomePageRowsSelected();
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (ref.current) ref.current.indeterminate = some && !all; // v9 は && !all が必要
}, [some, all]);
<input ref={ref} type="checkbox" checked={all} onChange={table.getToggleAllPageRowsSelectedHandler()} />
Shift クリックの範囲選択が既定で有効になった
v8 では自前で書く必要があった範囲選択が、v9 では row.getToggleSelectedHandler() に組み込まれました。demo で 1 行目をクリックしてから 10 行目を Shift + クリックすると、間の行も含めて 10 行 が選択されました(追加のコードなし)。範囲選択が不要なら enableRowRangeSelection: false を渡します。row.toggleSelected() を直接呼んでいる箇所は従来どおりです。
4. Pagination
const table = useTable({
...,
initialState: { pagination: { pageIndex: 0, pageSize: 8 } },
});
// JSX
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>前</button>
<span>{table.state.pagination.pageIndex + 1} / {table.getPageCount()}</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>次</button>
サーバー側ページングしたい場合は manualPagination: true + pageCount(または rowCount)を渡す。このとき toggleAllRowsSelected が選べるのは 手元に届いているページの行だけ です。「検索結果 12,000 件すべて」をサーバー側で選ばせたいなら、行 id の集合ではなく「検索条件 + 除外 id」をサーバーに送る設計にします。
5. ColumnDef でセルのカスタムレンダリング
columns の cell に JSX を返す関数を渡せば、セル単位で表示を組み替えできる(アイコン / リンク / バッジ等)。v9 は型の第 1 引数に typeof features が入ります:
const columns: ColumnDef<typeof features, 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で誤操作防止rowSelectionの key は row.id:getRowIdを渡さないと row.id は配列の index になり、データを並べ替えて渡し直すと別の行が選択されたことになる。getRowId: (row) => String(row.id)で安定 ID にするgetIsSomeRowsSelected()は v9 で意味が変わった:中間状態はsome && !all- ドラッグ中の state 名は
columnResizing:v8 のcolumnSizingInfo/onColumnSizingInfoChangeを使っていたら改名する - virtual scroll と組み合わせる時は
@tanstack/react-virtualを別途追加(react-virtuoso よりこちらが Table と相性よい)
関連 Topic / 関連書籍
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
React Hooks ベースで Web アプリを手を動かしながら実装する
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発
『TypeScript と React/Next.js でつくる実践 Web アプリケーション開発』(2022 年 7 月、技術評論社)は
改訂新版 これからはじめるReact実践入門 : コンポーネントの基本からNext.jsによるアプリ開発まで
React 19 / Next.js 15 対応の基礎から本格アプリ開発を体系的に習得する