mcpField
W
web-ui-tanstack-table

web-ui-tanstack-table

github.com/agents-inc開発2676 回インストール2 日前に更新
リポジトリ

web-ui-tanstack-table について

TanStack Table v8で型付きのヘッドレステーブルを構築するための実践ガイドです。行モデルやテーブル状態、クライアント側とサーバー側の処理を扱い、マークアップやスタイルは規定しません。

主な機能

  1. 01型付きカラム定義
  2. 02ソート、フィルター、ページネーション
  3. 03行選択と展開
  4. 04カラムの表示切り替えと固定
  5. 05サーバー側データのパターン
  6. 06安定した参照と行 ID

活用シーン

  1. 01独自のテーブルマークアップの構築
  2. 02テーブル状態とサーバークエリの連携
  3. 03選択と一括操作の追加
  4. 04大規模テーブルの応答性維持

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill web-ui-tanstack-table

インストール後、Claude Code で/web-ui-tanstack-tableを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
column-pinning.md
column-resizing.md
column-visibility.md
core.md
expanding.md
filtering.md
pagination.md
selection.md
server-side.md
sorting.md
virtualization.md

詳細

TanStack Table のパターン

クイックガイド: TanStack Table v8 はテーブルの ロジック を担います。行、セル、ヘッダーグループを計算しますが、描画は一切行いません。createColumnHelper<T>() でデータを定義し、各機能の行モデルをインポートして有効にします。インスタンスが返す値を使って、好きなマークアップを描画できます。問題の多くは、次の 2 点に起因します。data と columns は安定した参照でなければならず、そうでないとテーブルが再計算を繰り返します。また、サーバー側ですでに処理済みであることを示すのは manual* フラグです。

詳細なリソース:


該当する方法を選ぶ

ソート、フィルタリング、ページングをどこで行うかによって、そのほかの答えがすべて変わります。

  • クライアントがすべての行を保持します。 必要な機能の行モデル(getSortedRowModel、getFilteredRowModel、getPaginationRowModel)をインポートすると、テーブルが処理します。examples/core.md と機能ごとのファイルを参照してください。
  • サーバー側ですでに処理済みです。 manualSorting、manualFiltering、manualPagination を true に設定して rowCount を指定し、これらの行モデルはインポートしません。manual* フラグを設定すると、対応する行モデルは無視されます。そのため両方を含めるのは無駄で、機能が動作しているように見えてしまいます。examples/server-side.md を参照してください。
  • すべての行がそろっていますが、描画するには多すぎます。 クライアント側の行モデルを維持し、ページングではなく出力をウィンドウ化します。examples/virtualization.md を参照してください。

<critical_requirements>

TanStack Table のコードを書く前に

data と columns の参照を安定させてください — useMemo を使うか、モジュールレベルの定数にします。テーブルは同一性で比較するため、レンダーのたびに新しい配列を作ると、レンダーのたびに新しい列セットになります。その結果、エラーではなくタブが応答しなくなるような更新ループが発生します。

列ヘルパーに型を指定してください:createColumnHelper<Row>()。 これにより、各アクセサーキーが行の型に照らしてチェックされ、各セルの getValue() にも読み取るフィールドの型が付与されます。フィールド名の変更が、undefined の列ではなくコンパイルエラーになります。

accessorFn には明示的な id を設定してください。 文字列の accessorKey には独自の ID が設定されますが、関数には ID を導き出す名前がありません。ID がなければ、ソート、フィルタリング、表示設定でその列を指定できません。

行を選択する場合やデータを再読み込みする場合は、getRowId を設定してください。 デフォルトの識別子は配列のインデックスです。そのため、ソート、フィルター、再取得を行うと、選択状態がその位置に新たに移動してきた行へ、気付かないうちに移ります。

使用する行モデルだけをインポートしてください。 それぞれが独立したエントリーポイントであり、使わないものはツリーシェイキングで除外されます。このようなパッケージ構成になっている理由がまさにここにあります。 flexRender でレンダリングします。 header と cell はそれぞれ文字列またはコンポーネントにできます。どちらにも対応するのが flexRender です。columnDef.header を直接読み取る方法は、関数を指定する最初の列が現れるまでは機能します。

</critical_requirements>


自動検出: @tanstack/react-table, useReactTable, createColumnHelper, columnHelper.accessor, columnHelper.display, columnHelper.group, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, flexRender, ColumnDef, SortingState, ColumnFiltersState, PaginationState, RowSelectionState, VisibilityState, ExpandedState, ColumnPinningState, manualPagination, manualSorting, accessorKey, accessorFn, getRowId

対象:

  • 列の定義 — アクセサー、計算値、表示列、グループ化されたヘッダー
  • テーブルの状態:ソート、フィルタリング、ページネーション、選択、展開、表示設定、ピン留め、サイズ設定
  • クライアント側とサーバー側のどちらで処理するかの選択と、後者の接続
  • ヘッダーグループ、表示中のセル、行モデルを使った、レンダリングのためのインスタンスの読み取り
  • メモ化、ツリーシェイキング、行出力のウィンドウ化による、大規模なテーブルの応答性の維持

別途扱う項目:

  • すべての要素とスタイル。このライブラリが返すのはデータ構造です。例にある <table> はそれらをレンダリングする方法のひとつであり、結果の見た目はこのスキルの対象外です
  • 行の取得元。サーバー側モードでは、どのような手段で取得したかにかかわらず、すでにページネーションされた配列と合計件数を受け取ります
  • フィルターやページネーションの入力に使うコントロール。テーブルが必要とするのは値と変更ハンドラーだけで、それらを生成するウィジェットの詳細は問いません
  • ウィンドウ化そのもの。テーブルから渡されるのはフラットな行配列です。ビューポートを測定して、収まる範囲をレンダリングする機能は別の機能であり、examples/virtualization.md では両者をつなぐ方法のみを示します

ヘッドレスとは、ライブラリの仕様にこちらが合わせる必要がないということです。上書きするテーマも、対処しなければならないマークアップも、内部にアクセスする必要のあるコンポーネントもありません。インスタンスに質問し、何を描画するかは自分で決められます。

その代わり、デフォルトで有効になっている機能はありません。 ソートされないテーブルは壊れているのではなく、getSortedRowModel をインポートしていないだけです。この点は、機能を切り替えるための props が用意されているコンポーネントライブラリから移ってきた人がつまずきやすいところです。覚えておくとよいでしょう。機能が動作しないときは、まず行モデルが設定されているかを確認し、次に manual* フラグによってテーブルが処理を行わないよう指定されていないかを確認します。

同じ設計により、ライブラリはフレームワークに依存しません。コアはプレーンな TypeScript で、React アダプターは薄い層です。そのため、API は処理を行うフックではなく、データを返す関数として構成されています。


<decision_framework>

インポートする行モデル

機能行モデル備考
すべてのケースgetCoreRowModel常に必須
ソートgetSortedRowModelmanualSorting を設定する場合は省略
列およびグローバルフィルタリングgetFilteredRowModelmanualFiltering を設定する場合は省略
ページ分割getPaginationRowModelmanualPagination を設定する場合は省略
展開可能な行、サブ行getExpandedRowModel
行のグループ化getGroupedRowModel
フィルター UI 向けの個別値getFacetedRowModel
列のピン留め、列のサイズ設定なしどちらもコアに含まれています

列の宣言方法

Reading a field directly?          → columnHelper.accessor("email")
Reading a nested field?            → columnHelper.accessor("address.city")   (dot notation)
Deriving a value from the row?     → columnHelper.accessor(fn, { id: "…" })  (id is required)
No data at all — buttons, a checkbox?
                                   → columnHelper.display({ id: "…" })
Heading a set of other columns?    → columnHelper.group({ columns: [...] })

重要な違いは、アクセサーの戻り値がソートとフィルタリングの対象になるため、プリミティブ値のままにすることです。表示に関するものはすべて cell に記述します。

</decision_framework>


基本パターン

パターン 1:テーブルのセットアップ

インスタンスは、データ、列、および選択した行モデルから構築されます。どちらの入力もメモ化されます。getRowId を使うと、インデックスベースの識別子を安定した識別子に置き換えられます。

const columnHelper = createColumnHelper<User>();

const columns = useMemo(
  () => [
    columnHelper.accessor("firstName", { header: "First Name" }),
    columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
      id: "fullName", // required — a function has no key to derive one from
      header: "Full Name",
    }),
  ],
  [],
);

const table = useReactTable({
  data: useMemo(() => users, [users]),
  columns,
  getCoreRowModel: getCoreRowModel(),
  getRowId: (row) => row.id,
});

コード全文:examples/core.md

パターン 2:ソート

状態を親に引き上げると永続化できます。並べ替えは行モデルが行います。

const [sorting, setSorting] = useState<SortingState>([]);

const table = useReactTable({
  /* … */
  state: { sorting },
  onSortingChange: setSorting,
  getSortedRowModel: getSortedRowModel(),
});

columnHelper.accessor("createdAt", { sortingFn: "datetime" });

Date オブジェクトには sortingFn: "datetime" が必要です。デフォルトの比較関数はこれらを文字列として扱うため、曜日名で並べ替えられます。

コード全文:examples/sorting.md

パターン 3:フィルタリング

列フィルタとグローバルフィルタは、それぞれ別の状態とハンドラーを持ち、どちらも同じ行モデルによって処理されます。

const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState("");

const table = useReactTable({
  /* … */
  state: { columnFilters, globalFilter },
  onColumnFiltersChange: setColumnFilters,
  onGlobalFilterChange: setGlobalFilter,
  getFilteredRowModel: getFilteredRowModel(),
});

複数の列フィルタは積集合になります。つまり、行はすべてのフィルタを満たす必要があります。組み込みの和集合はありません。和集合を実現するには、グローバルフィルタかカスタムの filterFn を使用します。

コード全文:examples/filtering.md

パターン 4:ページネーション
const [pagination, setPagination] = useState<PaginationState>({
  pageIndex: 0,
  pageSize: 10,
});

const table = useReactTable({
  /* … */
  state: { pagination },
  onPaginationChange: setPagination,
  getPaginationRowModel: getPaginationRowModel(),
});

pageIndex はゼロから数えます。多くの API は 1 から数えるため、変換はリクエストの境界でのみ行います。

コード全文:examples/pagination.md

パターン 5:行の選択
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const table = useReactTable({
  /* … */
  state: { rowSelection },
  onRowSelectionChange: setRowSelection,
  enableRowSelection: true,
  getRowId: (row) => row.id, // the selection is keyed by this
});

選択状態は行 ID をキーとするマップです。そのため、getRowId がないと配列内の位置がキーになります。テーブルをソートすると、各選択状態が別のレコードに割り当てられてしまいます。

コード全文:examples/selection.md

パターン 6:サーバーサイドデータ

manual* フラグは、渡された配列がすでにソート、フィルタリング、ページ分割されていることをテーブルに伝えます。その場合、テーブルは状態の変化を通知するだけで、計算は行いません。

const table = useReactTable({
  data: apiData ?? [],
  columns,
  state: { pagination, sorting, columnFilters },
  onPaginationChange: setPagination,
  onSortingChange: setSorting,
  onColumnFiltersChange: setColumnFilters,
  getCoreRowModel: getCoreRowModel(),
  manualPagination: true,
  manualSorting: true,
  manualFiltering: true,
  rowCount: totalFromApi, // it cannot count rows it never received
});

コード全文:examples/server-side.md

パターン 7:列の表示切り替え
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
  email: false, // hidden until asked for
});

columnHelper.accessor("id", { enableHiding: false }); // never offered in the toggle UI

非表示の列は getVisibleCells() から除外されますが、テーブル内には残るため、その列のフィルタやソートは引き続き適用されます。

コード全文:examples/column-visibility.md

パターン 8:行の展開
const [expanded, setExpanded] = useState<ExpandedState>({});

const table = useReactTable({
  /* … */
  state: { expanded },
  onExpandedChange: setExpanded,
  getExpandedRowModel: getExpandedRowModel(),
  getRowCanExpand: () => true, // or a predicate over row.original
});

展開された行はネストされず、行モデル内に挿入されます。そのため、詳細パネルを描画するには、row.getIsExpanded() を確認して自分で追加の要素を出力します。

コード全文:examples/expanding.md

パターン 9:再利用可能なジェネリックテーブル

ジェネリクスを使うと、呼び出し側での型推論を損なうことなく、1 つのコンポーネントをあらゆる行の型に使用できます。

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function DataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });
  // render from table.getHeaderGroups() and table.getRowModel()
}

コード全文:examples/core.md

パターン 10:列の固定
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
  left: ["id"],
  right: ["actions"],
});

固定すると列の順序が変わり、各列がどちら側にあるかが通知されます。位置指定は適用されません。position: sticky、オフセット、不透明な背景は自分で設定する必要があります。スクロール中のコンテンツが透けて見えないようにするのが、この背景の役割です。

コード全文:examples/column-pinning.md

パターン 11:列の幅変更
const table = useReactTable({
  /* … */
  enableColumnResizing: true,
  columnResizeMode: "onChange", // or "onEnd"
});

"onChange" はドラッグ中に更新され、そのたびにすべてのセルが再レンダリングされるため、フレームレートを維持するには CSS 変数を使う方法と、メモ化したボディが必要です。"onEnd" はドラッグを離したときに確定するため、どちらも必要ありません。

コード全文:examples/column-resizing.md


<red_flags>

注意すべき問題

実行時に動作しなくなるもの:

  • columns または data をインラインで構築する — レンダリングのたびに新しい配列 ID が生成され、タブが応答しなくなるまでテーブルがループ更新されます
  • id のない accessorFn — カラムに識別子がないため、インスタンスの構築中に例外が発生します
  • manual* フラグとクライアント側の行モデルを併用する — フラグが優先されるため、機能が接続されているように見えても行モデルは機能しません
  • rowCount または pageCount のない manualPagination — テーブルは単一ページからページ数を算出できないため、ページ移動が最初のページで止まります
  • getRowId を指定しない選択 — 配列インデックスをキーとして使うため、並べ替えや再取得によって選択対象が別のレコードに移ります
  • アクセサーから JSX を返す — 並べ替えとフィルタリングで値ではなく React 要素が比較されるため、カラムが意味のある基準で並び替えられません
  • flexRender を使わずに columnDef.header または columnDef.cell を直接読み取る — 文字列のヘッダーでは動作しますが、それ以外のカラムではすべて関数がそのまま表示されます

予想外の動作:

  • pageIndex はゼロ始まりですが、多くの API は 1 始まりです
  • autoResetPageIndex の既定値は true で、データが変更されるたびに最初のページへ戻ります — 新しいデータの到着自体がページ変更となるサーバーサイドモードでは、通常望ましくありません
  • カラムフィルターは AND 条件で組み合わされます。組み込みの OR 条件はありません
  • カラムの固定とサイズ変更には、他のすべての機能と異なり、行モデルは必要ありません
  • 固定はカラムの並べ替えとグループ化より先に行われるため、両者が競合する場合は固定が優先されます
  • サイズ変更ハンドルには getResizeHandler() の onMouseDown と onTouchStart の両方が必要です。そうしないと、タッチデバイスでは機能しません
  • 非表示のカラムもフィルタリングと並べ替えの対象です。表示設定が影響するのは getVisibleCells() のみで、それより上流には影響しません

コメント