원클릭으로
tanstack-table
Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | tanstack-table |
| description | Headless UI for building powerful tables & datagrids for TS/JS, React, Vue, Solid, Svelte, Qwik, Angular, and Lit. |
TanStack Table is a headless UI library for building data tables and datagrids. It provides logic for sorting, filtering, pagination, grouping, expanding, column pinning/ordering/visibility/resizing, and row selection - without rendering any markup or styles.
Package: @tanstack/react-table
Utilities: @tanstack/match-sorter-utils (fuzzy filtering)
Current Version: v8
npm install @tanstack/react-table
// WRONG - new references every render, causes infinite loops
const table = useReactTable({
data: fetchedData.results, // new ref!
columns: [{ accessorKey: 'name' }], // new ref!
})
// CORRECT - stable references
const columns = useMemo(() => [...], [])
const data = useMemo(() => fetchedData?.results ?? [], [fetchedData])
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
import { createColumnHelper } from '@tanstack/react-table'
type Person = {
firstName: string
lastName: string
age: number
status: 'active' | 'inactive'
}
const columnHelper = createColumnHelper<Person>()
const columns = [
// Accessor column (data column)
columnHelper.accessor('firstName', {
header: 'First Name',
cell: info => info.getValue(),
footer: info => info.column.id,
}),
// Accessor with function
columnHelper.accessor(row => row.lastName, {
id: 'lastName', // required with accessorFn
header: () => <span>Last Name</span>,
cell: info => <i>{info.getValue()}</i>,
}),
// Display column (no data, custom rendering)
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<button onClick={() => deleteRow(row.original)}>Delete</button>
),
}),
// Group column (nested headers)
columnHelper.group({
id: 'info',
header: 'Info',
columns: [
columnHelper.accessor('age', { header: 'Age' }),
columnHelper.accessor('status', { header: 'Status' }),
],
}),
]
| Option | Type | Description |
|---|---|---|
id | string | Unique identifier (auto-derived from accessorKey) |
accessorKey | string | Dot-notation path to row data |
accessorFn | (row) => any | Custom accessor function |
header | string | (context) => ReactNode | Header renderer |
cell | (context) => ReactNode | Cell renderer |
footer | (context) => ReactNode | Footer renderer |
size | number | Default width (default: 150) |
minSize | number | Min width (default: 20) |
maxSize | number | Max width |
enableSorting | boolean | Enable sorting |
sortingFn | string | SortingFn | Sort function |
enableFiltering | boolean | Enable filtering |
filterFn | string | FilterFn | Filter function |
enableGrouping | boolean | Enable grouping |
aggregationFn | string | AggregationFn | Aggregation function |
enableHiding | boolean | Enable visibility toggle |
enableResizing | boolean | Enable resizing |
enablePinning | boolean | Enable pinning |
meta | any | Custom metadata |
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, // For server-side sorting
});
// Built-in sort functions: 'alphanumeric', 'text', 'datetime', 'basic'
// Column-level: sortingFn: 'alphanumeric'
const table = useReactTable({
state: { columnFilters },
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
})
// Built-in: 'includesString', 'equalsString', 'arrIncludes', 'inNumberRange', etc.
// Filter 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(),
// For server-side:
// manualPagination: true,
// pageCount: serverPageCount,
});
// Navigation
table.nextPage();
table.previousPage();
table.firstPage();
table.lastPage();
table.setPageSize(20);
table.getCanNextPage(); // boolean
table.getCanPreviousPage(); // boolean
table.getPageCount(); // total pages
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const table = useReactTable({
state: { rowSelection },
onRowSelectionChange: setRowSelection,
enableRowSelection: true,
enableMultiRowSelection: true,
})
// Checkbox column
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()}
/>
),
})
// Get selected rows
table.getSelectedRowModel().rows
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const table = useReactTable({
state: { columnVisibility },
onColumnVisibilityChange: setColumnVisibility,
})
// Toggle 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,
});
// Render pinned sections separately
row.getLeftVisibleCells(); // Left-pinned
row.getCenterVisibleCells(); // Unpinned
row.getRightVisibleCells(); // Right-pinned
const table = useReactTable({
enableColumnResizing: true,
columnResizeMode: 'onChange', // 'onChange' | 'onEnd'
defaultColumn: { size: 150, minSize: 50, maxSize: 500 },
})
// Resize handle in header
<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(),
});
// Built-in aggregation: '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, // For hierarchical data
})
// Expand toggle
<button onClick={row.getToggleExpandedHandler()}>
{row.getIsExpanded() ? '−' : '+'}
</button>
// Detail row pattern
{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(),
// Do NOT include getSortedRowModel, getFilteredRowModel, getPaginationRowModel
});
// Fetch data based on state
useEffect(() => {
fetchData({ sorting, filters: columnFilters, pagination });
}, [sorting, columnFilters, pagination]);
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 and columns to prevent infinite re-rendersflexRender for all header/cell/footer renderingtable.getRowModel().rows for final rendered rows (not getCoreRowModel)getRowId for stable row keys when data has unique IDsmanualX options for server-side operationsstate.X and onXChangeautoResetPageIndex: true when filtering should reset paginationgetCoreRowModel() (required for all tables)id when using accessorFnmanualPagination with client-side getPaginationRowModelcolSpan for grouped headersheader.isPlaceholder for group column spacersDesign engineering principles for making interfaces feel polished. Use when building UI components, reviewing frontend code, implementing animations, hover states, shadows, borders, typography, micro-interactions, enter/exit animations, or any visual detail work. Triggers on UI polish, design details, "make it feel better", "feels off", stagger animations, border radius, optical alignment, font smoothing, tabular numbers, image outlines, box shadows.
Project scaffolding CLI with 30+ integrations, custom templates, and MCP server for AI agents.
Opinionated toolkit for building, versioning, and publishing high-quality JavaScript/TypeScript packages.
Reactive client-first store for your API with collections, live queries, and optimistic mutations.
Centralized, extensible devtools panel for TanStack libraries with a plugin architecture.
Framework-agnostic debouncing, throttling, rate limiting, queuing, and batching utilities.