開発

TanStack Tableとは:v9の導入コードとヘッドレス設計・AG Gridとの選び分け

TanStack Tableとは:v9の導入コードとヘッドレス設計・AG Gridとの選び分け

TanStack Tableは、一覧表の並べ替え・絞り込み・ページ送りといった状態と計算だけを受け持ち、画面の描画は開発者に任せる「ヘッドレス」のテーブルライブラリです。旧名はReact Tableで、ReactのほかVue・Svelte・Angular・Solidなどでも動きます。2026年8月4日に安定版のv9が公開され、10月29日にnpm registryで確認した最新版は9.3.1でした。この記事では、ヘッドレスという設計の意味、v9での導入コード、v8までの記事のコードが動かない理由、サーバー側ページングと仮想スクロールの組み方、AG Gridを選ぶべき場面との境界までを扱います。業務システムの一覧画面を作る開発者と、その部品選定を任された担当者に向けた内容です。

まとめ:TanStack Tableを採用する条件とAG Gridなど既製グリッドを選ぶ境界

TanStack Tableが向くのは、自社のデザインシステムやUIライブラリに合わせて一覧画面の見た目を自分で作りたい案件です。並べ替え、絞り込み、ページ送り、行選択、列の表示切り替えといったロジックは揃っており、HTMLとCSSは完全に手元で決められます。ライセンスはMITで、本番環境でも無償です。v9では使う機能だけを登録する方式になり、読み込むコードを小さく抑えられます。

反対に、セル編集、列幅のドラッグ操作の見た目、仮想スクロールまで一式で欲しいなら、既製の部品を持つAG Gridのほうが早く仕上がります。TanStack Tableは描画を持たないため、編集用の入力欄も行の仮想化も自分で組むからです。また、2026年10月時点で検索上位にある日本語記事の多くはv8のuseReactTable前提で、v9ではそのままでは動きません。

TanStack Tableの定義とヘッドレス設計・v9.3系のパッケージ構成

まず、ヘッドレスという考え方と、名前と版の変遷を押さえます。ここを曖昧にしたまま古い記事を読むと、v7・v8・v9のコードが混ざって動かなくなります。

表示部品を持たず状態と計算だけを渡すヘッドレス型の設計と役割

公式のOverviewは、TanStack TableはAG Gridのような作り込み済みのテーブル部品ではない、と明言しています。ライブラリが返すのは「どの列で並べ替え中か」「今は何ページ目か」「絞り込み後に残る行はどれか」といった状態と、その計算結果です。<table>で描くか、<div>の格子で描くか、カード型にするかは利用者が決めます。

この方式の利点は、見た目の制約が無いことです。既製のグリッドは独自のDOM構造とCSSを持つため、社内のUIライブラリと揃えようとすると上書きのCSSが増えます。TanStack Tableなら、既存のボタンや入力欄の部品をそのままヘッダーやセルに置けます。代わりに、マークアップ、スタイル、アクセシビリティ属性はすべて自分で書くことになるのです。

旧React Table v7からv9までの名前と版の変遷・npm実測値

名前が変わったため、パッケージを取り違えやすいのが注意点です。npm registryで2026年10月29日に確認した値を表にしました。

版 パッケージ名 公開日 扱い
7.8.0 react-table 2022年5月16日 旧名・更新停止
8.0.0 @tanstack/react-table 2022年6月27日 現行名の初版
8.21.3 @tanstack/react-table 2025年4月14日 v8の最終版
9.0.0 @tanstack/react-table 2026年8月4日 v9の安定版
9.3.1 @tanstack/react-table 2026年10月12日 最新版

本体のロジックは@tanstack/table-coreにあり、Reactなら@tanstack/react-table、Vueなら@tanstack/vue-tableという薄いアダプタ経由で使います。公式のInstallationによると、v9のReact版はReact 18以上、Vue版は3.2以上、Angular版は19以上が対象です。Svelte 3と4を使う案件はv8に留まるよう案内されています。GitHubリポジトリのスター数は同日時点で28,532件でした。

v9で変わった機能の登録方式と5kbから始まるバンドルの構成要素

v9最大の変更は、使う機能をtableFeatures()で明示的に登録する方式になった点です。公式の移行ガイドでは、v8は全機能を常に同梱していたのに対し、v9は「5kbから始めて必要な機能だけを足す」と説明されています。並べ替えしか使わない表には絞り込みやページ送りのコードが入りません。

同じガイドによると、大きな表でメモリ使用量が最大90%減り、並べ替えや絞り込みの処理が40〜70%速くなる場面があるとのことです。配布形式はESMのみになり、CommonJSとUMDの版は廃止されました。古いビルド環境でrequire()から読み込んでいる案件は、移行前にバンドラの設定を確認してください。npmの配布物は、Reactアダプタが圧縮状態で29,513バイト、コアが218,686バイトでした。

npmでv9を入れてReactで受注一覧を表示する最小コードの書き方

受注一覧を例に、表示から並べ替え・検索・ページ送りまでのコードを示します。ViteとTypeScriptのReactプロジェクトを想定しています。

インストールとtableFeaturesで列定義から表を描くまでの手順

公式のQuick Startに沿って、Reactアダプタを入れます。コアは依存として自動で入ります。

npm install @tanstack/react-table@9

次のコンポーネントをそのまま貼れば、3行の一覧が表示されます。v9では、機能を使わない表でもtableFeatures({})を渡すのが必須です。

// OrdersTable.tsx:受注一覧の最小構成
import { tableFeatures, useTable } from '@tanstack/react-table'
import type { ColumnDef } from '@tanstack/react-table'

type Order = { code: string; name: string; qty: number; status: string }

// data はモジュール直下・useState・useQuery などで参照を安定させる
const data: Array<Order> = [
  { code: 'A-001', name: '六角ボルトM8', qty: 120, status: '受付' },
  { code: 'A-002', name: '平ワッシャーM8', qty: 240, status: '手配中' },
  { code: 'B-010', name: '結束バンド200mm', qty: 50, status: '出荷済' },
]

// v9:この表で使う機能を宣言する(まだ何も使わない)
const features = tableFeatures({})

const columns: Array<ColumnDef<typeof features, Order>> = [
  { accessorKey: 'code', header: '品番' },
  { accessorKey: 'name', header: '品名' },
  { accessorKey: 'qty', header: '数量', cell: (info) => info.getValue<number>().toLocaleString() },
  { accessorKey: 'status', header: '状態' },
]

export function OrdersTable() {
  const table = useTable({ features, columns, data })

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map((headerGroup) => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <th key={header.id}>
                {header.isPlaceholder ? null : <table.FlexRender header={header} />}
              </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>
  )
}

table.FlexRenderは、列定義のheaderやcellが文字列でも関数でも描画する部品です。dataを描画のたびに新しい配列として作ると、表が毎回作り直されて重くなります。コメントのとおり、参照が変わらない場所に置いてください。

並べ替え・検索・ページ送りの機能を登録して業務の一覧画面に足す

機能を足すときは、機能本体と、その機能が使う行モデルを同じtableFeatures()に並べます。行モデルとは、元のデータに並べ替えや絞り込みを適用した結果の行の集まりです。全体検索は列の絞り込みに依存するため、両方を登録します。

// features の差し替え:並べ替え・全体検索・ページ送り
import {
  columnFilteringFeature, createFilteredRowModel, createPaginatedRowModel,
  createSortedRowModel, filterFn_includesString, globalFilteringFeature,
  rowPaginationFeature, rowSortingFeature, sortFn_alphanumeric, tableFeatures,
} from '@tanstack/react-table'

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns: { alphanumeric: sortFn_alphanumeric },
  columnFilteringFeature,
  globalFilteringFeature,
  filteredRowModel: createFilteredRowModel(),
  filterFns: { includesString: filterFn_includesString },
  rowPaginationFeature,
  paginatedRowModel: createPaginatedRowModel(),
})

// 画面側:見出しクリックで並べ替え、入力欄で全体検索、ボタンでページ送り
<input placeholder="品番・品名で検索" onChange={(e) => table.setGlobalFilter(e.target.value)} />
<th onClick={header.column.getToggleSortingHandler()}>
  <table.FlexRender header={header} />
  {{ asc: ' ▲', desc: ' ▼' }[header.column.getIsSorted() as string] ?? null}
</th>
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>前へ</button>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>次へ</button>

ページサイズの既定値は10行です。配布物のrowPaginationFeatureで既定値を確認しました。業務画面ではinitialState: { pagination: { pageIndex: 0, pageSize: 50 } }のように、最初から件数を指定することが多いでしょう。並べ替え関数と絞り込み関数は、使うものだけを個別に登録すると、使わない処理がバンドルに入りません。

v8のuseReactTableとgetCoreRowModelの記事が動かない理由

検索で見つかる記事のコードをv9で動かすと、まずuseReactTableが見つからないというエラーになります。9.3.1の配布物にこの名前は無く、useTableに改名されました。次に、v8で必須だったgetCoreRowModel: getCoreRowModel()は不要になり、getSortedRowModel()などはtableFeatures()のsortedRowModel: createSortedRowModel()に置き換わっています。

見落としやすいのは、行やセルのメソッドを分割代入する書き方です。v9ではメモリ削減のため、メソッドが各オブジェクトではなくプロトタイプに置かれました。const { getValue } = rowのように取り出すとthisを失って動きません。row.getValue('name')のように、必ずオブジェクト経由で呼んでください。段階的に移すなら、@tanstack/react-table/legacyのuseLegacyTableがv8の書き方を受け付けます。ただし非推奨の一時措置で、全機能を含むためバンドルはv8より大きくなる点に注意が必要です。全機能を有効にしたいだけならstockFeaturesを渡す方法もあります。

サーバー側ページングと仮想スクロールで大量行の一覧を組む手順

件数の多い一覧では、データを全部ブラウザに送るか、サーバーで絞ってから送るかを先に決めます。TanStack Tableはどちらにも対応しますが、データの取得そのものは行いません。

manualPaginationとrowCountでAPIの検索結果をページ送りする

公式のPaginationガイドでは、サーバー側でページを切る場合、manualPagination: trueを指定し、総件数をrowCountで渡します。総件数を渡さないと、表は最終ページを判断できません。取得にはTanStack Queryを組み合わせるのが公式の例です。

// OrdersServerTable.tsx:並べ替えとページ送りをサーバーに任せる
import { useState } from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { rowPaginationFeature, rowSortingFeature, tableFeatures, useTable } from '@tanstack/react-table'
import type { ColumnDef, PaginationState, SortingState } from '@tanstack/react-table'

const features = tableFeatures({ rowPaginationFeature, rowSortingFeature })
const columns: Array<ColumnDef<typeof features, Order>> = [/* 最小構成と同じ列定義 */]

async function fetchOrders(p: PaginationState, s: SortingState) {
  const params = new URLSearchParams({
    page: String(p.pageIndex + 1),
    size: String(p.pageSize),
    sort: s[0] ? `${s[0].id}:${s[0].desc ? 'desc' : 'asc'}` : '',
  })
  const res = await fetch(`/api/orders?${params}`)
  return res.json() as Promise<{ rows: Array<Order>; rowCount: number }>
}

export function OrdersServerTable() {
  const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 50 })
  const [sorting, setSorting] = useState<SortingState>([])

  const query = useQuery({
    queryKey: ['orders', pagination, sorting],
    queryFn: () => fetchOrders(pagination, sorting),
    placeholderData: keepPreviousData, // ページ切り替え中に表を空にしない
  })

  const table = useTable({
    features,
    columns,
    data: query.data?.rows ?? [],
    rowCount: query.data?.rowCount,
    state: { pagination, sorting },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
    manualPagination: true,
    manualSorting: true,
  })
  // 描画は最小構成と同じ
}

ここでの注意は、並べ替えもサーバー側に寄せる点です。ページを切るのがサーバーなのに並べ替えだけをブラウザで行うと、表示中の50行の中で並ぶだけで、全件の並び順にはなりません。公式ガイドも、ページ送りをサーバーが持つなら、絞り込みと並べ替えもサーバーが持つべきだと述べています。API側のオフセット方式とカーソル方式の選び方はページネーション設計とはで整理しています。

TanStack Virtualで見えている行だけ描画する組み合わせ方

TanStack Tableには行の仮想化が含まれません。数千行をページ送りせずにスクロールで見せたい場合は、同じ開発元のTanStack Virtualを組み合わせます。2026年10月29日時点の@tanstack/react-virtualは3.14系でした。先ほどのtbody部分を、次のように見えている行だけ描く形に置き換えます。

// 仮想スクロール:表示範囲の行だけを描画する(ページ送りの機能は登録しない)
import { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'

const scrollRef = useRef<HTMLDivElement>(null)
const rows = table.getRowModel().rows
const virtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => scrollRef.current,
  estimateSize: () => 36, // 1行の高さの見込み(px)
  overscan: 10,           // 画面外に余分に描く行数
})

return (
  <div ref={scrollRef} style={{ height: 480, overflow: 'auto' }}>
    <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
      {virtualizer.getVirtualItems().map((item) => {
        const row = rows[item.index]
        return (
          <div key={row.id} style={{ position: 'absolute', top: item.start, height: item.size, display: 'flex', width: '100%' }}>
            {row.getAllCells().map((cell) => (
              <div key={cell.id} style={{ flex: 1 }}><table.FlexRender cell={cell} /></div>
            ))}
          </div>
        )
      })}
    </div>
  </div>
)

行を絶対配置で並べるため、<table>要素のままでは列幅が崩れやすく、<div>の格子で描く例が一般的です。スクロールする要素に高さを指定し忘れると、全行が描かれて仮想化の意味がなくなります。AG Gridではこの部分が最初から組み込まれており、ここが両者の手間の差として最も大きく出ます。

クライアント処理とサーバー処理を分ける行数以外の業務上の判断軸

公式のClient-Side vs Server-Sideガイドは、数千行程度ならブラウザ側の処理で十分なことが多いとしています。全機能を100万行のクライアント処理で負荷試験しており、v9では1,500万行まで扱えるというのも同ガイドの記述です。ただし同ガイドは、境界を決めるのは行数ではないとも述べています。見るべきは、転送量、ブラウザのメモリ、行モデルの計算時間、DOMの描画量、サーバー側の検索コストの5つです。

業務システムに当てはめると、判断は次のようになります。全件を送ってよい権限があり、転送が数MB以内に収まるなら、クライアント処理と仮想スクロールで操作が速い画面にできます。権限によって見える行が変わる、件数が日々増え続ける、検索条件が監査の対象になる、のいずれかに当たるなら、サーバー処理とページ送りを選んでください。

TanStack TableとAG Grid・Handsontableを案件で選び分ける軸

同じG48グループで扱った3つのライブラリを、一覧画面の作り手の視点で比べます。

自前で書く量・バンドル・ライセンスで比べる3ライブラリの比較表

観点 TanStack Table AG Grid Handsontable
描画 自分で書く 部品が持つ 部品が持つ
仮想スクロール 別ライブラリで組む 標準で備える 標準で備える
セル編集 入力欄を自作 無償版で可 主機能
見た目の自由度 制約なし テーマで調整 テーマで調整
ライセンス MIT 無償版MIT・有償版あり 商用は有償

AG Gridの機能差とEnterprise版の判断はAG Gridとは、Excel風の入力が主役の画面はHandsontableとはで詳しく扱っています。AG Gridの無償版は、圧縮後でも約523KBの配布ファイルでした。TanStack Tableは必要な機能だけを登録できるため、読み込み量では有利です。その差を、描画とスクロールを自分で書く手間と引き換えにしていると考えてください。

採用条件は社内のデザインシステムに合わせた一覧を自前で持つ画面

TanStack Tableを選ぶのは、次の条件が2つ以上当てはまる案件です。1つ目は、社内のUIライブラリやデザインシステムがあり、一覧画面もその部品で統一したいという要件が当てはまります。2つ目は、カード表示とテーブル表示を切り替えるなど、表の形に収まらない描き方が要る場合です。3つ目は、TanStack RouterやTanStack Formと型の書き方を揃えたいという要件が当てはまります。4つ目は、読み込むJavaScriptの量に厳しい制約がある場合です。

v9では型の扱いも強化の対象です。登録していない機能のメソッドは型に現れないため、ページ送りを登録していない表でnextPage()を呼ぶと、コンパイルの段階で誤りに気づけます。同じ設定の表を何画面も作るなら、createTableHookで機能と部品をまとめた独自フックを作っておくと、画面ごとの記述が減ります。

見送る場面は編集中心・短納期・集計やExcel出力が要る画面

TanStack Tableを選ばないのは次の3場面です。セル編集が画面の中心なら、入力欄、キーボード移動、貼り付けの処理を一から書くことになるため、AG GridかHandsontableが早く仕上がります。納期が短く見た目の要件も緩いなら、既製部品の標準デザインで足ります。行のグループ化と集計、Excel形式の出力を作り込む手間が大きいなら、それらを部品として持つAG GridのEnterprise版と工数で比べてください。

どの部品を選んでも、権限ごとに見せる行、検索条件の記録、同時編集の排他は表の外側で設計する仕事です。一覧画面の操作感と、裏側のAPI・権限・監査ログを一体で決める場合は、業務用・Webアプリ開発として要件の段階から相談いただけます。業務アプリ全体の作り方の比較は業務アプリとはにまとめています。

よくある質問:TanStack Tableの無料利用・v8移行・Vue対応・編集機能

TanStack Tableの導入検討でよく出る質問に、2026年10月29日時点の公式情報をもとに答えます。

TanStack Tableは商用の業務システムでも無料で使えますか?

使えます。@tanstack/react-tableと@tanstack/table-coreはMITライセンスで、有償版や機能制限版はありません。社内システムでも顧客向けサービスでも同じです。著作権表示とライセンス文の同梱という、MITライセンスの条件だけは守ってください。

v8で作った画面はすぐにv9へ移行すべきですか?

急ぐ必要はありません。v8の最終版8.21.3は2025年4月から更新が止まっていますが、動いている画面が壊れるわけではないためです。新規画面はv9で書き、既存画面は改修の機会にuseTableとtableFeatures()へ移すのが現実的です。ESMのみの配布になったため、ビルド環境の確認を先に済ませてください。

VueやAngularでもTanStack Tableは使えますか?

使えます。Vueは@tanstack/vue-table、Angularは@tanstack/angular-tableを入れます。v9の対象はVue 3.2以上、Angular 19以上です。フレームワークの選定から迷っている場合はReactとVue 3の違いを先に確認してください。

TanStack Tableにセル編集の機能はありますか?

専用の編集機能はありません。セルの描画で<input>を置き、変更時にデータを更新する関数を呼ぶ形で自作します。数列の簡単な修正なら十分ですが、範囲の貼り付けや連続入力が要る画面では作る量が一気に増えます。v9で追加されたcellSelectionFeatureは範囲選択の状態管理で、編集そのものは含みません。

何行までのデータなら快適に表示できますか?

ブラウザ側の計算は、公式が100万行で負荷試験しているため、業務システムの多くの件数で問題になりません。重くなる原因はDOMの描画で、数百行を超えて1画面に並べるなら、ページ送りかTanStack Virtualによる仮想スクロールを入れてください。全件を送れないデータは、サーバー側ページングを選びます。

関連記事

お気に入りに入れた記事の一覧

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  2. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  3. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  4. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  5. 2026.10.28 テックブログ 楽天トラベル不正アクセス:最大約260万件の対象確認とカード下4桁の詐欺対策

RELATED POSTS 関連記事

目次