← ブログ一覧

TanStack Table v9移行で踏んだ落とし穴

TanStack Table v9.1.2への移行をVite+React+TSのサンドボックスで実際に検証。型エラー・legacyサブパスへの書き換え・バンドルサイズ実測まで正直に記録した。


この記事の結論: TanStack Table v9は「オプトイン化でバンドル削減」と謳われているが、v8互換のlegacyサブパスに載せ替えただけではバンドルサイズは減らず、むしろ実測で約27%増えた。新しいuseTable系APIへの本格移行は今回未検証。また移行前の型チェックは、ルートのtsconfig.jsontsc --noEmit -p .するだけだと素通りしてしまうため、npm run buildかアプリ側のtsconfig.app.jsonを直接指定する必要がある。

リナ社長(高校生社長のやり手ギャル)× タクヤ(入社3年目)— 今回は、TanStack Table v9安定版をサンドボックスで実際にv8から移行させてみる回。(登場人物について

「タクヤ、TanStack Tableのv9が出たらしいじゃん。『状態管理を刷新してバンドル軽くなりました』みたいな触れ込み見たんだけど、これウチのミニロトのバックテスト結果表示にも使えそう?」

「見ましたね、v9.1.2です。ただ、公式の触れ込みをそのまま信じるのは危ないので、実際に移行を試してみたんですよ。結果から言うと、そんなに単純な話じゃなかったです」


1. TanStack Table v9で何が変わったのか(公式の触れ込み)

TanStack Table v9系では、内部の状態管理がTanStack Storeベースに刷新されたと公式が発表している。公式GitHub Releasesページ(table-core@9.1.2)では、状態更新をsetStateSliceというユーティリティに集約し、値が変化していない更新はスキップする「structural no-op policy」を導入したことが確認できる。あわせて、機能をオプトインで組み込む方式に変えることでバンドルサイズを削減できる、という触れ込みだった。

「状態管理の中身とバンドル削減の話は一次情報のリリースノートで確認できるんですけど、それが実際のアプリでどう効いてくるかは、手を動かしてみないと分からないので検証しました」とタクヤ。

2. 実際にv8プロジェクトをv9に移行してみる(サンドボックス検証)

環境はVite 8.2.2 + React 19 + TypeScript 6.0.2の新規プロジェクト。まずv8を入れて、sorting・filtering・paginationを使う基本的なテーブルコンポーネントをuseReactTable + getCoreRowModel/getSortedRowModel/getFilteredRowModel/getPaginationRowModel + createColumnHelperで実装した。

$ npm create vite@latest . -- --template react-ts
$ npm install @tanstack/react-table@8
# インストール後: "version": "8.21.3"
$ npx tsc --noEmit -p tsconfig.app.json
(エラーなし)

型チェックが通ることを確認したうえで、コードは一切変えずにv9へ上げてみる。

$ npm install @tanstack/react-table@9
# インストール後: "version": "9.1.2"

まずViteスカフォールドのデフォルトどおり、ルートのtsconfig.jsonを指定してtsc --noEmit -p .を実行した。

$ npx tsc --noEmit -p .
(エラーなし、EXIT CODE: 0)

「これだけ見ると、コード変えなくてもv9で通っちゃうじゃんって思うよね」とリナ社長。ところがnpm run build(内部でtsc -b && vite buildを実行)を叩くと状況が一変した。

$ npm run build
src/Table.tsx(3,3): error TS2724: '"@tanstack/react-table"' has no exported member named 'useReactTable'. Did you mean 'ReactTable'?
src/Table.tsx(4,3): error TS2724: '"@tanstack/react-table"' has no exported member named 'getCoreRowModel'. Did you mean 'createCoreRowModel'?
src/Table.tsx(5,3): error TS2724: '"@tanstack/react-table"' has no exported member named 'getSortedRowModel'. Did you mean 'createSortedRowModel'?
src/Table.tsx(6,3): error TS2724: '"@tanstack/react-table"' has no exported member named 'getFilteredRowModel'. Did you mean 'createFilteredRowModel'?
src/Table.tsx(7,3): error TS2305: Module '"@tanstack/react-table"' has no exported member 'getPaginationRowModel'.
src/Table.tsx(21,41): error TS2558: Expected 2 type arguments, but got 1.
(以下、型推論が壊れたことによる暗黙anyエラーが複数)

原因を調べると、Viteスカフォールドのルートtsconfig.json"files": []とproject referencesだけのsolution-style構成で、tsc --noEmit -p .ではそもそも1ファイルもコンパイル対象になっていなかった(--listFilesで確認しても対象ファイルが出ない)。つまりさっきの「エラーなし」は、チェックした結果ゼロ件だったのではなく、何も検査していなかっただけの偽の緑(false green)だった。

実際にコードを直すため、v8互換のlegacyサブパスに切り替えた。node_modules/@tanstack/react-table/package.jsonexportsマップで"./legacy": "./dist/legacy.js"という専用エントリがあることを確認したうえで、以下のように書き換えている。

- import {
-   useReactTable,
-   getCoreRowModel,
-   getSortedRowModel,
-   getFilteredRowModel,
-   getPaginationRowModel,
-   flexRender,
-   createColumnHelper,
-   type SortingState,
-   type ColumnFiltersState,
- } from "@tanstack/react-table";
+ import { flexRender } from "@tanstack/react-table";
+ import {
+   useLegacyTable,
+   getCoreRowModel,
+   getSortedRowModel,
+   getFilteredRowModel,
+   getPaginationRowModel,
+   legacyCreateColumnHelper,
+ } from "@tanstack/react-table/legacy";
+ import type { SortingState, ColumnFiltersState, ColumnDef } from "@tanstack/table-core";
+ import type { LegacyFeatures } from "@tanstack/react-table/legacy";

- const columnHelper = createColumnHelper<Person>();
+ const columnHelper = legacyCreateColumnHelper<Person>();

- const columns = [
+ const columns: ColumnDef<LegacyFeatures, Person, any>[] = [
    columnHelper.accessor("name", { header: "Name" }),
    columnHelper.accessor("age", { header: "Age" }),
  ];

- const table = useReactTable({
+ const table = useLegacyTable({

関数名だけでなく、ColumnDefの型引数が2つ(ColumnDef<TData, TValue>)から3つ(ColumnDef<TFeatures, TData, TValue>)に増えている点も踏まえて修正した。この修正後、ビルドが通った。

$ npm run build
✓ 122 modules transformed.
dist/assets/index-BgD_SOcH.js   307.60 kB │ gzip: 90.55 kB
✓ built in 577ms

さらに、同一のsorting/filtering/pagination付きテーブル1個だけの単純なアプリで、v8素のAPIとv9のlegacyサブパス経由のバンドルサイズを比較した。

v8(useReactTable等、素のv8 API):
✓ 19 modules transformed.
dist/assets/index-B8w148_G.js   241.97 kB │ gzip: 73.21 kB

v9(legacyサブパス経由でv8スタイルを再現):
✓ 122 modules transformed.
dist/assets/index-BgD_SOcH.js   307.60 kB │ gzip: 90.55 kB

v9のlegacy互換パスを使うと、v8よりビルド後のJSが約65.6KB(gzip後約17.3KB)、率にして約27%大きくなっていた。新しいuseTable/createTableHookベースのAPIへ本格移行した場合の数値は、今回は時間の都合で検証していない。

3. タクヤ視点の技術ポイント/リナ社長視点のビジネスインパクト

「一番びっくりしたのは、v8スタイルの関数APIが@tanstack/react-tableのメインエントリポイントから完全になくなってたことです。useReactTablecreateColumnHelperも、代わりにuseTableReactTablecreateTableHookという新しい体系に置き換わってる。v8互換のコードは別のlegacyサブパスに温存されていて、名前までuseLegacyTableのように変わっているので、単純なリネームでは済まないんですよ」とタクヤ。

「で、それウチら(読者)の何が変わるの?バンドル減るって言われてたのに増えてるじゃん、これ」とリナ社長が切り込む。

「そこなんです。今回確認できたのは、legacyサブパスで動かす限りはバンドル削減の恩恵はないということだけです。新APIに完全移行すれば減るかもしれませんが、そこは今回確認していません。だから『バンドルが減るから今すぐ上げよう』という判断はまだできないですね」

「移行工数の面でも、v8のコードをそのまま置いてもnpm run buildは通らなかったわけでしょ?importと関数名と型引数、全部書き換えが要るってことは、ウチのミニロトのバックテスト結果表示みたいなテーブルUIに手を入れるなら、それなりに時間見ておかないとヤバいってことだよね」

「はい。今回はAstro側では試していないので断言はできませんが、少なくともサンドボックスで見た限り、legacyパスへの置き換えだけでも書き換え量はそこそこありました」

4. まとめ・持ち帰り

TanStack Table v9への移行を検討するときは、まずnpm run build(またはアプリ側のtsconfig.jsonを直接指定したtsc --noEmit)で型エラーを確認すること。ルートのtsconfig.json相手にtsc --noEmit -p .するだけでは、Viteのsolution-style構成だとチェックが素通りしてしまう。またv8互換のlegacyサブパスは移行の一時しのぎにはなるが、バンドルサイズ削減の恩恵は今回の検証では確認できなかった(むしろ増えた)ので、「バンドルが減るから今すぐ上げよう」と思っている場合は実測してから判断すべきだ。

編集部としての立場は明確にしておきたい。バンドル削減を目当てに急いでv9へ上げる必要はない。状態管理の刷新(TanStack Store化)は内部設計として大きな変更であり、legacyパスに逃げれば動くとはいえ、型システムの変更点も無視できない規模だった。時間を確保して検証したうえで上げるのが無難というのが今回の結論だ。

「みんなのプロジェクトのtsconfig.json、ルート直でtsc --noEmitしても本当に全ファイルチェックできてる?一回--listFilesで確認してみない?」とリナ社長。この問いは今回のライブラリに限らず、Viteのsolution-style構成を使っているプロジェクト全般に当てはまる話だ。


参考

  • TanStack Table Releases — v9.1.2(table-core@9.1.2)の変更内容(setStateSlice、structural no-op policyなど)を確認

コメント