TanStack Table v8 で構築する高性能な React データグリッド

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Reactにおける大規模データセットの課題

フロントエンド開発者の多くは、いずれデータテーブルの壁に突き当たります。50行程度であれば .map() で問題なく動作しますが、大規模データセットが1,000件、10,000件と増えてくると、標準的なアプローチでは限界が来ます。ブラウザの動作が重くなり、操作にラグが生じ、DOMが管理しきれないほど肥大化するためです。

TanStack Table v8 は、こうしたパフォーマンスのボトルネックを解決するための最適な選択肢です。これは「ヘッドレス(headless)」ライブラリであり、特定のUIを強制することなく、テーブルのロジック、状態、APIのみを管理します。そのため、HTMLとCSSを完全に制御できます。実際のプロダクション環境では、10,000行を超えるデータセットを処理する場合でも、このアプローチによってインターフェースの軽快さを維持できることを確認しています。

ステップ1:インストールとプロジェクトのセットアップ

まず、コアとなるテーブルパッケージをインストールします。大量のデータをスムーズに処理したいため、同じエコシステムの仮想化(virtualization)パッケージも追加します。

npm install @tanstack/react-table @tanstack/react-virtual

yarnやpnpmを使用している場合は、以下のコマンドを使用してください:

yarn add @tanstack/react-table @tanstack/react-virtual
# または
pnpm add @tanstack/react-table @tanstack/react-virtual

バージョン8は最初から TypeScript で構築されています。これにより優れた型安全性が提供され、デプロイ後ではなく開発中に潜在的なバグを見つけることができます。

ステップ2:設定とコアロジック

TanStack Table v8 の最大の強みは、そのモジュール性にあります。必要な機能だけをインポートするため、バンドルサイズを小さく抑えられます。ここでは、ソートとフィルタリングを処理する設定を行ってみましょう。

カラムとデータの定義

まずはデータ構造を定義します。セルレンダラーをクリーンかつ型安全に保つために、createColumnHelper を使用します。

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

type User = {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  status: string;
};

const columnHelper = createColumnHelper<User>();

const columns = [
  columnHelper.accessor('id', {
    header: () => <span>ID</span>,
    cell: info => info.getValue(),
  }),
  columnHelper.accessor('firstName', {
    header: '名',
  }),
  columnHelper.accessor('lastName', {
    header: '姓',
  }),
  columnHelper.accessor('email', {
    header: 'メールアドレス',
  }),
  columnHelper.accessor('age', {
    header: '年齢',
  }),
];

テーブルフックの実装

useReactTable フックは、コンポーネントの頭脳だと考えてください。ここでソートやフィルタリングのモデルを紐付けます。ライブラリがモジュール化されているため、使用する行モデル(row models)を明示的に指定する必要があります。

import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  SortingState,
} from '@tanstack/react-table';
import { useState } from 'react';

function DataTable({ data }) {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [globalFilter, setGlobalFilter] = useState('');

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

  return (
    <div className="p-4">
      <input
        value={globalFilter ?? ''}
        onChange={e => setGlobalFilter(e.target.value)}
        placeholder="すべてのカラムを検索..."
        className="mb-4 p-2 border rounded"
      />
      <table>
        {/* ここにヘッダーと行が入ります */}
      </table>
    </div>
  );
}

仮想化による高速化

5,000行を一度にレンダリングすると、5,000個の <tr> 要素が作成されます。15列のテーブルであれば計75,000個のDOMノードが発生し、モバイルブラウザではクラッシュする可能性もあります。仮想化(Virtualization)は、ビューポートに現在表示されている行だけをレンダリングすることで、この問題を解決します。

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

const tableContainerRef = useRef<HTMLDivElement>(null);
const { rows } = table.getRowModel();

const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => tableContainerRef.current,
  estimateSize: () => 35, // 1行あたりの平均の高さ(ピクセル)
  overscan: 10, // スムーズなスクロールのために10行をプリレンダリング
});

const virtualRows = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();

JSX内では、仮想化ツール(virtualizer)から取得した start 値を使用して、行に絶対配置(absolute position)を適用します。これにより、DOMを軽量に保ちながら、ブラウザには大きなスクロール領域があるように見せかけることができます。

ステップ3:検証とモニタリング

仮想化の設定が終わったら、パフォーマンスの向上が実際に得られているか確認する必要があります。私は通常、実装が効果的であることを確認するために、以下のチェックリストに従っています。

DOMノード数の確認

Chrome DevToolsを開き、「Elements」タブを確認してください。リストをスクロールしてみましょう。10,000件のアイテムがあっても、DOM内に一度に存在する <tr> 要素が30個程度であれば、仮想化は正しく機能しています。これにより、メモリ使用量が大幅に削減されます。

コンポーネントの再レンダリングの監視

フィルタリングやソートを行う際に、React Profiler を使用してセッションを記録します。個々のセルが不必要に再レンダリングされていると、パフォーマンスが低下します。これを防ぐには、カラムオブジェクトをコンポーネントの外で定義するか、useMemo でラップしてください。

よくある落とし穴を避ける

  • CSSの不足: 親コンテナーに固定の高さ(height)と overflow: auto が設定されていないと、仮想化は機能しません。
  • 不安定なカラム定義: useMemo を使わずにコンポーネント内でカラムを定義すると、レンダリングのたびにテーブルがリセットされてしまいます。
  • 重いロジック: アクセッサー関数(accessor functions)は高速に保ってください。複雑なデータ変換はバックエンドで行うか、テーブルの状態に渡す前に処理しておきましょう。

これらの手順に従うことで、レスポンスが良くプロフェッショナルなデータグリッドを構築できます。UIがフリーズすることなく、ユーザーは膨大なデータセットをスクロールできるようになります。このセットアップは、後にカラムのリサイズや行のグループ化などの機能を追加するための、クリーンな基盤にもなります。

Share: