TanStack Table v8をマスターする:高機能なデータテーブルを手軽に構築する方法

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Reactにおけるデータテーブルという「悩み」

フロントエンド開発者にとって、Next.js 15 App Routerなどの最新環境であっても、データテーブル(Data Table)の実装は常に最も骨の折れるタスクの一つです。数行の静的なデータを表示するだけなら、標準の <table> タグで十分です。しかし、ソート(sorting)、フィルタリング(filtering)、ページネーション(pagination)といった要件が加わると、ロジックと状態管理(state)の複雑な迷路に迷い込むことになります。

以前、約5万行のコードを持つプロジェクトのリファクタリングに参加したことがあります。そこでは、データテーブルがあらゆる種類のUIライブラリで継ぎ接ぎされていました。その時の手痛い教訓は、ESLintとPrettierの設定の迷宮に迷い込むのと同様に、「最初にツール選びを間違えると、後のメンテナンスで表示バグ一つ直すのに一週間も費やすことになる」ということです。TanStack Table v8(旧 React Table)は、全く異なるアプローチでこの問題を解決するために誕生しました。

テーブル構築における3つの一般的なアプローチ

コードを書き始める前に、なぜ専門家が TanStack Table を優先するのか、一般的な手法を振り返ってみましょう。

1. 純粋なHTMLテーブルを使用する

  • メリット: 非常に軽量で、バンドルサイズを全く消費しません。
  • デメリット: すべてのロジックを自分で書く必要があります。ソート機能だけでもコードが長大になり、バグが発生しやすくなります。

2. UIライブラリ(MUI, Ant Design, Chakra UIなど)のコンポーネントを使用する

  • メリット: すぐに使えます。コピー&ペーストするだけで、洗練されたインターフェースが手に入ります。
  • デメリット: 深いカスタマイズが非常に困難です。デザイナーからテンプレートにない「独特な」レイアウトを求められた場合、CSSの上書き(CSS override)に多大な労力を費やすことになります。

3. Headless UI (TanStack Table v8) を使用する

  • メリット: これは「UIを持たない」ライブラリです。ロジックと状態管理のみを担当し、どのように表示するかは開発者に委ねられます。Tailwind CSSを使いたいですか?CSS Modulesですか?それとも単なる <div> ですか?すべて自由自在です。
  • デメリット: 初期設定(ボイラープレートコード)に少し時間がかかります。

なぜ TanStack Table v8 は「使う価値」があるのか?

実際、TanStack Table v8 はわずか 14-15kb 程度と軽量ながら、非常に強力です。TypeScriptをフルサポートしており、Type-Safeなフォームバリデーションを実装する際と同様に、データのマッピングミスを即座に検出できます。最大の利点は、ロジックとUIを完全に分離できることです。UIコンポーネントを分離して開発することで、特定の規格に縛られることなく、独自のテーブル用デザインシステムを構築できます。

「見た目が綺麗だから」という理由だけでUIライブラリを選ばないでください。プロジェクトの要件が頻繁に変わる可能性がある場合、柔軟性のないライブラリを使用することは、リファクタリング時の悪夢に変わります。

TanStack Table v8 実装ガイド A to Z

まずは、プロジェクトにライブラリをインストールしましょう。

npm install @tanstack/react-table

ステップ 1: カラム構造の定義

TanStack Table において、columns は設計図のような役割を果たします。どこからデータを取得し、どのように表示するかを定義します。

import { createColumnHelper } from '@tanstack/react-table';

const data = [
  { id: 1, name: '田中 太郎', email: '[email protected]', role: '管理者' },
  { id: 2, name: '佐藤 花子', email: '[email protected]', role: '編集者' },
];

const columnHelper = createColumnHelper();

const columns = [
  columnHelper.accessor('id', {
    header: 'ID',
    cell: info => info.getValue(),
  }),
  columnHelper.accessor('name', {
    header: '氏名',
    cell: info => <span className="font-bold">{info.getValue()}</span>,
  }),
  columnHelper.accessor('email', {
    header: 'メールアドレス',
  }),
];

ステップ 2: テーブルインスタンスの初期化

useReactTable フックを使用して、テーブルのすべての状態を制御します。

import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';

function MyDataTable() {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <table className="w-full border-collapse">
      <thead>
        {table.getHeaderGroups().map(headerGroup => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map(header => (
              <th key={header.id} className="border p-2 bg-gray-50">
                {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} className="border p-2">
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

ステップ 3: ソート機能(Sorting)の追加

ソートを有効にするには、getSortedRowModel を追加し、React の useState を介して sorting 状態を管理する必要があります。

const [sorting, setSorting] = useState([]);

const table = useReactTable({
  data,
  columns,
  state: { sorting },
  onSortingChange: setSorting,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
});

ステップ 4: フィルタリング機能(Filtering)の追加

フィルタリング機能には getFilteredRowModel が必要です。全カラムを対象としたクイック検索(Global Filter)用のシンプルな入力欄を作成できます。

const [globalFilter, setGlobalFilter] = useState('');

const table = useReactTable({
  state: { sorting, globalFilter },
  onGlobalFilterChange: setGlobalFilter,
  getFilteredRowModel: getFilteredRowModel(),
  // ... その他の設定
});

ステップ 5: ページネーション機能(Pagination)の追加

ページネーションは、数千行のデータを一度に処理する際のブラウザの「ラグ」を防ぐのに役立ちますが、より計算コストの高い処理が必要な場合はWeb Workersの併用も有効です。

const table = useReactTable({
  getPaginationRowModel: getPaginationRowModel(),
  initialState: { pagination: { pageSize: 10 } },
  // ... その他の設定
});

大規模データを扱う際の実践的なアドバイス

APIからの実データを扱う際、リストが1万行を超えるような場合は、クライアント側でソートやページネーションを行わないでください。代わりに Manual Pagination を使用しましょう。TanStack Table には manualPagination: true のようなフラグがあり、テーブルの状態をサーバーと直接同期させることができます。

私がよく使うテクニックは、React Query との組み合わせです。ユーザーがページを切り替えると、React Query が自動的に新しいデータをフェッチします。この方法は、アプリケーションをよりスムーズかつプロフェッショナルなものにします。

最後に、カラムとデータを memoize してパフォーマンスを最適化しましょう。ユーザーが検索ボックスに1文字入力するたびにテーブル全体が再レンダリングされることは、UIの最適化において避けるべき事態です。

この記事が「Headless UI」の考え方を理解する助けになれば幸いです。カラムのリサイズ(Column Resizing)や行の選択(Row Selection)などの高度な機能にもぜひ挑戦して、ユーザーエクスペリエンスを次のレベルへと引き上げてください。

Share: