tech-book-labs
● ガイド(データユーティリティ) · 2 セクション統合

TanStack Table v9 実装ガイド(v8 からの移行つき)

TanStack Table v9(headless table)で柔軟なテーブル UI を組むパターン。useReactTable から useTable への書き換え、バンドルサイズの実測、列幅 / 列表示 / 行選択、「全選択」が選ぶ行数の実測までを 1 ページに統合。

著者:TechBook.net編集部 · 最終検証 2026-09-19
● セクション · 2026-09-19 · @tanstack/react-table 9.2.4

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 で型チェックと動作確認を通したものです。

触って試す

IDNameRoleCommits
1Alicefrontend142
2Bobbackend88
3Charliedevops211
4Danafrontend65
5Erindata197
6Frankbackend34
ヘッダクリックでソート / 上のテキストで全列フィルタ

ヘッダクリックで sort、上のテキストボックスで全列フィルタ。

この実装を体系立てて学ぶ本(ライブラリの API を追う前に、React のレンダリングと状態の考え方を固めると設計で迷わない)

tech-book.net /books/9784815635947

改訂新版 これからはじめるReact実践入門 : コンポーネントの基本からNext.jsによるアプリ開発まで

山田 祥寛 · SBクリエイティブ · 2025年

React 19 / Next.js 15 対応の基礎から本格アプリ開発を体系的に習得する

この本が役立つ理由 — コンポーネントの基本から Next.js まで。改訂新版で React 19 世代の書き方
詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks / Eve Porcello / 宮崎 空 · オライリー・ジャパン · 2021年

React Hooks ベースで Web アプリを手を動かしながら実装する

この本が役立つ理由 — Hooks・状態管理・テストまで通しで。実務の設計判断の根拠になる
詳細を tech-book.net で見る

v8 から何が変わったか

公式の 移行ガイド から、この記事の範囲で効くものだけ抜き出します。

v8v9
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().paginationtable.state.pagination
flexRender(h.column.columnDef.header, h.getContext())<table.FlexRender header={h} />(flexRender も残っている)
const { getValue } = rowrow.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 で確かめました:

登録した sortFnsname 昇順の結果
alphanumeric + basic + textAlice, 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 に紐づく書籍:

tech-book.net /books/9784815635947

改訂新版 これからはじめるReact実践入門 : コンポーネントの基本からNext.jsによるアプリ開発まで

山田 祥寛 · SBクリエイティブ · 2025年

React 19 / Next.js 15 対応の基礎から本格アプリ開発を体系的に習得する

この本が役立つ理由 — コンポーネントの基本から Next.js まで。改訂新版で React 19 世代の書き方
詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks / Eve Porcello / 宮崎 空 · オライリー・ジャパン · 2021年

React Hooks ベースで Web アプリを手を動かしながら実装する

この本が役立つ理由 — Hooks・状態管理・テストまで通しで。実務の設計判断の根拠になる
詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也 / 吉田 健人 / 高林 佳稀 · 技術評論社 · 2022年

『TypeScript と React/Next.js でつくる実践 Web アプリケーション開発』(2022 年 7 月、技術評論社)は

この本が役立つ理由 — TypeScript × React/Next.js で 1 本のアプリを作り切る
詳細を tech-book.net で見る
tech-book.net /books/9784844379546

【POD】React & Gatsby開発入門

竹本 雄貴

Gatsby でサイトを作りながら React のマークアップを習得する

詳細を tech-book.net で見る
● セクション · 2026-09-19 · @tanstack/react-table 9.2.4

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 編 にあります。

触って試す

列の表示:
IDNameRoleCommitsReviewsUpdated
1Alicefrontend842026-09-23
2Bobbackend39212026-09-20
3Charliedevops70382026-09-17
4Danadata101552026-09-14
5Erindesign132722026-09-11
6Frankfrontend16392026-09-08
7Gracebackend194262026-09-05
8Alicedevops225432026-09-02
1–8 /47 件
1 / 6
ヘッダ右端の縦線をドラッグで列幅変更 / チェックボックスで列の表示切替 / 行のチェックで選択

ヘッダ右端の縦線を ドラッグで列幅変更、上のチェックボックスで 列表示切替、行のチェックボックスで 選択、下のボタンで ページング。

この実装を体系立てて学ぶ本(機能を足すほど state が増える。React の状態の持ち方を先に固めておくと、どこに何を置くかで迷わない)

tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks / Eve Porcello / 宮崎 空 · オライリー・ジャパン · 2021年

React Hooks ベースで Web アプリを手を動かしながら実装する

この本が役立つ理由 — Hooks・状態管理・テストまで通しで。実務の設計判断の根拠になる
詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也 / 吉田 健人 / 高林 佳稀 · 技術評論社 · 2022年

『TypeScript と React/Next.js でつくる実践 Web アプリケーション開発』(2022 年 7 月、技術評論社)は

この本が役立つ理由 — TypeScript × React/Next.js で 1 本のアプリを作り切る。管理画面の型づけの参考に
詳細を tech-book.net で見る

機能を 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 / 関連書籍

tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks / Eve Porcello / 宮崎 空 · オライリー・ジャパン · 2021年

React Hooks ベースで Web アプリを手を動かしながら実装する

この本が役立つ理由 — Hooks・状態管理・テストまで通しで。実務の設計判断の根拠になる
詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也 / 吉田 健人 / 高林 佳稀 · 技術評論社 · 2022年

『TypeScript と React/Next.js でつくる実践 Web アプリケーション開発』(2022 年 7 月、技術評論社)は

この本が役立つ理由 — TypeScript × React/Next.js で 1 本のアプリを作り切る
詳細を tech-book.net で見る
tech-book.net /books/9784815635947

改訂新版 これからはじめるReact実践入門 : コンポーネントの基本からNext.jsによるアプリ開発まで

山田 祥寛 · SBクリエイティブ · 2025年

React 19 / Next.js 15 対応の基礎から本格アプリ開発を体系的に習得する

この本が役立つ理由 — コンポーネントの基本から Next.js まで。改訂新版で React 19 世代の書き方
詳細を tech-book.net で見る

全ガイドは ガイド一覧 から。連載は 連載一覧 へ。