tanstack-table
TypeScript/JavaScriptおよびReact、Vue、Solid、Svelte、Qwik、Angular、Litに対応した、高機能なテーブル・データグリッドを構築するためのHeadless UIライブラリです。スタイルに依存しない設計により、柔軟なカスタマイズが可能なデータ表示コンポーネントを実装できます。
description の原文を見る
Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit.
SKILL.md 本文
概要
TanStack Table は、データテーブルとデータグリッドを構築するためのヘッドレス UI ライブラリです。ソート、フィルタリング、ページネーション、グループ化、展開、列ピン留め・並べ替え・表示非表示・リサイズ、行選択のロジックを提供しますが、マークアップやスタイルは一切レンダリングしません。
パッケージ: @tanstack/react-table
ユーティリティ: @tanstack/match-sorter-utils (ファジーフィルタリング)
現在のバージョン: v8
インストール
npm install @tanstack/react-table
コアアーキテクチャ
ビルディングブロック
- Column Definitions - 列を記述 (データアクセス、レンダリング、機能)
- Table Instance - ステート and APIs を持つ中央コーディネーター
- Row Models - データ処理パイプライン (フィルタ → ソート → グループ → ページネーション)
- Headers, Rows, Cells - レンダリング可能な単位
重要:データと列の安定性
// 間違い - 毎回レンダリング時に新しい参照が作られ、無限ループの原因になる
const table = useReactTable({
data: fetchedData.results, // 新しい参照!
columns: [{ accessorKey: 'name' }], // 新しい参照!
})
// 正しい - 安定した参照
const columns = useMemo(() => [...], [])
const data = useMemo(() => fetchedData?.results ?? [], [fetchedData])
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
列定義
createColumnHelper を使用する (推奨)
import { createColumnHelper } from '@tanstack/react-table'
type Person = {
firstName: string
lastName: string
age: number
status: 'active' | 'inactive'
}
const columnHelper = createColumnHelper<Person>()
const columns = [
// アクセッサ列 (データ列)
columnHelper.accessor('firstName', {
header: 'First Name',
cell: info => info.getValue(),
footer: info => info.column.id,
}),
// 関数を使用したアクセッサ
columnHelper.accessor(row => row.lastName, {
id: 'lastName', // accessorFn を使用する場合は必須
header: () => <span>Last Name</span>,
cell: info => <i>{info.getValue()}</i>,
}),
// ディスプレイ列 (データなし、カスタムレンダリング)
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<button onClick={() => deleteRow(row.original)}>Delete</button>
),
}),
// グループ列 (ネストされたヘッダー)
columnHelper.group({
id: 'info',
header: 'Info',
columns: [
columnHelper.accessor('age', { header: 'Age' }),
columnHelper.accessor('status', { header: 'Status' }),
],
}),
]
列オプション
| オプション | 型 | 説明 |
|---|---|---|
id | string | 一意な識別子 (accessorKey から自動導出) |
accessorKey | string | ドット記法でのローデータへのパス |
accessorFn | (row) => any | カスタムアクセッサ関数 |
header | string | (context) => ReactNode | ヘッダーレンダラー |
cell | (context) => ReactNode | セルレンダラー |
footer | (context) => ReactNode | フッターレンダラー |
size | number | デフォルト幅 (デフォルト: 150) |
minSize | number | 最小幅 (デフォルト: 20) |
maxSize | number | 最大幅 |
enableSorting | boolean | ソート有効化 |
sortingFn | string | SortingFn | ソート関数 |
enableFiltering | boolean | フィルタリング有効化 |
filterFn | string | FilterFn | フィルター関数 |
enableGrouping | boolean | グループ化有効化 |
aggregationFn | string | AggregationFn | 集約関数 |
enableHiding | boolean | 表示非表示トグル有効化 |
enableResizing | boolean | リサイズ有効化 |
enablePinning | boolean | ピン留め有効化 |
meta | any | カスタムメタデータ |
テーブルインスタンス
テーブルの作成
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
} from '@tanstack/react-table'
function MyTable() {
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = useReactTable({
data,
columns,
state: { sorting, columnFilters, pagination },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return (
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
{header.isPlaceholder ? null :
flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: ' ↑', desc: ' ↓' }[header.column.getIsSorted() as string] ?? null}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
ソート
const table = useReactTable({
state: { sorting },
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
enableMultiSort: true,
// manualSorting: true, // サーバーサイドソート用
})
// 組み込みソート関数: 'alphanumeric', 'text', 'datetime', 'basic'
// 列レベル: sortingFn: 'alphanumeric'
フィルタリング
列フィルタリング
const table = useReactTable({
state: { columnFilters },
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
})
// 組み込み: 'includesString', 'equalsString', 'arrIncludes', 'inNumberRange', など
// フィルター UI
function Filter({ column }) {
return (
<input
value={(column.getFilterValue() ?? '') as string}
onChange={e => column.setFilterValue(e.target.value)}
placeholder={`Filter... (${column.getFacetedUniqueValues()?.size})`}
/>
)
}
グローバルフィルタリング
const [globalFilter, setGlobalFilter] = useState('')
const table = useReactTable({
state: { globalFilter },
onGlobalFilterChange: setGlobalFilter,
globalFilterFn: 'includesString',
getFilteredRowModel: getFilteredRowModel(),
})
ファジーフィルタリング
import { rankItem } from '@tanstack/match-sorter-utils'
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value)
addMeta({ itemRank })
return itemRank.passed
}
const table = useReactTable({
filterFns: { fuzzy: fuzzyFilter },
globalFilterFn: 'fuzzy',
})
ページネーション
const table = useReactTable({
state: { pagination },
onPaginationChange: setPagination,
getPaginationRowModel: getPaginationRowModel(),
// サーバーサイド用:
// manualPagination: true,
// pageCount: serverPageCount,
})
// ナビゲーション
table.nextPage()
table.previousPage()
table.firstPage()
table.lastPage()
table.setPageSize(20)
table.getCanNextPage() // boolean
table.getCanPreviousPage() // boolean
table.getPageCount() // 総ページ数
行選択
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const table = useReactTable({
state: { rowSelection },
onRowSelectionChange: setRowSelection,
enableRowSelection: true,
enableMultiRowSelection: true,
})
// チェックボックス列
columnHelper.display({
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
/>
),
})
// 選択行を取得
table.getSelectedRowModel().rows
列の表示非表示
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const table = useReactTable({
state: { columnVisibility },
onColumnVisibilityChange: setColumnVisibility,
})
// トグル UI
{table.getAllLeafColumns().map(column => (
<label key={column.id}>
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/>
{column.id}
</label>
))}
列ピン留め
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
left: ['select', 'name'],
right: ['actions'],
})
const table = useReactTable({
state: { columnPinning },
onColumnPinningChange: setColumnPinning,
enableColumnPinning: true,
})
// ピン留めセクションを個別にレンダリング
row.getLeftVisibleCells() // 左ピン留め
row.getCenterVisibleCells() // ピン留めなし
row.getRightVisibleCells() // 右ピン留め
列リサイズ
const table = useReactTable({
enableColumnResizing: true,
columnResizeMode: 'onChange', // 'onChange' | 'onEnd'
defaultColumn: { size: 150, minSize: 50, maxSize: 500 },
})
// ヘッダーのリサイズハンドル
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`}
/>
グループ化と集約
const [grouping, setGrouping] = useState<GroupingState>([])
const table = useReactTable({
state: { grouping },
onGroupingChange: setGrouping,
getGroupedRowModel: getGroupedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
})
// 組み込み集約: 'sum', 'min', 'max', 'mean', 'median', 'count', 'unique', 'uniqueCount'
columnHelper.accessor('amount', {
aggregationFn: 'sum',
aggregatedCell: ({ getValue }) => `Total: ${getValue()}`,
})
行展開
const [expanded, setExpanded] = useState<ExpandedState>({})
const table = useReactTable({
state: { expanded },
onExpandedChange: setExpanded,
getExpandedRowModel: getExpandedRowModel(),
getSubRows: (row) => row.subRows, // 階層的なデータ用
})
// 展開トグル
<button onClick={row.getToggleExpandedHandler()}>
{row.getIsExpanded() ? '−' : '+'}
</button>
// 詳細行パターン
{row.getIsExpanded() && (
<tr>
<td colSpan={columns.length}>
<DetailComponent data={row.original} />
</td>
</tr>
)}
仮想化の統合
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualizedTable() {
const table = useReactTable({ /* ... */ })
const { rows } = table.getRowModel()
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
overscan: 10,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<table>
<tbody style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => {
const row = rows[virtualRow.index]
return (
<tr
key={row.id}
style={{
position: 'absolute',
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
サーバーサイド操作
const table = useReactTable({
data: serverData,
columns,
manualSorting: true,
manualFiltering: true,
manualPagination: true,
pageCount: serverPageCount,
state: { sorting, columnFilters, pagination },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
// getSortedRowModel, getFilteredRowModel, getPaginationRowModel は含めないこと
})
// ステートに基づいてデータを取得
useEffect(() => {
fetchData({ sorting, filters: columnFilters, pagination })
}, [sorting, columnFilters, pagination])
TypeScript パターン
列メタを拡張
declare module '@tanstack/react-table' {
interface ColumnMeta<TData extends RowData, TValue> {
filterVariant?: 'text' | 'range' | 'select'
align?: 'left' | 'center' | 'right'
}
}
カスタムフィルター・ソート関数の登録
declare module '@tanstack/react-table' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface SortingFns {
myCustomSort: SortingFn<unknown>
}
}
テーブルメタを経由した編集可能なセル
declare module '@tanstack/react-table' {
interface TableMeta<TData extends RowData> {
updateData: (rowIndex: number, columnId: string, value: unknown) => void
}
}
const table = useReactTable({
meta: {
updateData: (rowIndex, columnId, value) => {
setData(old => old.map((row, i) =>
i === rowIndex ? { ...row, [columnId]: value } : row
))
},
},
})
主なインポート
import {
createColumnHelper, flexRender, useReactTable,
getCoreRowModel, getSortedRowModel, getFilteredRowModel,
getPaginationRowModel, getGroupedRowModel, getExpandedRowModel,
getFacetedRowModel, getFacetedUniqueValues, getFacetedMinMaxValues,
} from '@tanstack/react-table'
import type {
ColumnDef, SortingState, ColumnFiltersState, VisibilityState,
PaginationState, ExpandedState, RowSelectionState, GroupingState,
ColumnOrderState, ColumnPinningState, FilterFn, SortingFn,
} from '@tanstack/react-table'
ベストプラクティス
- 常に
dataとcolumnsをメモ化 - 無限再レンダリングを防ぐ - すべてのヘッダー・セル・フッターレンダリングに
flexRenderを使用 getCoreRowModelではなくtable.getRowModel().rowsを使用 - 最終的なレンダリング行用- 必要な行モデルのみインポート - 各行はパイプラインに処理を追加
- データが一意な ID を持つ場合は
getRowIdを使用 - 安定した行キー用 - サーバーサイド操作に
manualXオプションを使用 - 制御されたステートを対にする -
state.XとonXChangeの両方 - カスタムメタ、フィルター fn、ソート fn にはモジュール拡張を使用
- 列定義に列ヘルパーを使用 - 型安全性のため
- フィルタリングがページネーションをリセットする場合は
autoResetPageIndex: trueを設定
よくある落とし穴
- 列をインラインで定義する (毎回レンダリング時に新しい参照が作られる)
getCoreRowModel()を忘れる (すべてのテーブルで必須)- 行モデルをインポートせずに使用する
accessorFn使用時にidを指定しないmanualPaginationとクライアント側のgetPaginationRowModelを混在させる- グループ化されたヘッダー用の
colSpanを忘れる header.isPlaceholderをグループ列スペーサーとして処理しない
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- tanstack-skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/tanstack-skills/tanstack-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。