Implements data tables with TanStack Table (React Table), including column definitions, client-side and server-side sorting/filtering/pagination, and integration with shadcn Table and TablePagination. Use when building tables, list views, data grids, or when the user mentions TanStack Table, React Table, or table columns.
Apply this skill when:
DataTable or TablePagination componenttable-columns.tsx, export a function that returns ColumnDef<T>[].{Feature}TableView.tsx uses useReactTable, passes the table instance to DataTable, and optionally TablePagination.DataTable<TData>({ table }) renders headers and rows via flexRender; use shadcn Table and support meta.className for column/cell styling.table-columns.tsxColumnDef<T>[] from @tanstack/react-table.onEdit, onDelete) and returns the column array so columns can close over callbacks.accessorKey for simple fields; use id + accessorFn for derived or nested fields (e.g. event_day.event_date).enableSorting: true where sorting is allowed; use meta: { className } for column/cell width and alignment (e.g. w-[120px], text-right).id: "actions", enableSorting: false, and render buttons with row.original.import { type ColumnDef } from "@tanstack/react-table";
import { format } from "date-fns";
export const itemColumns = (
onEdit: (item: Item) => void,
onDelete: (item: Item) => void
): ColumnDef<Item>[] => [
{
accessorKey: "id",
header: "ID",
enableSorting: true,
meta: { className: "font-semibold w-[80px]" }
},
{
accessorKey: "created_at",
header: "Created",
enableSorting: true,
cell: ({ row }) => format(new Date(row.getValue("created_at") as string), "MMM dd, yyyy")
},
{
id: "actions",
header: "Actions",
enableSorting: false,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<button type="button" aria-label="Edit" onClick={() => onEdit(row.original)}>Edit</button>
<button type="button" aria-label="Delete" onClick={() => onDelete(row.original)}>Delete</button>
</div>
),
meta: { className: "w-[120px] text-right" }
}
];
header: () => null, cell: () => null, and meta: { className: "hidden" }.sorting via useState<SortingState>([{ id: "created_at", desc: true }]), and searchQuery from parent.useMemo(() => featureColumns(onEdit, onDelete), [onEdit, onDelete]).data, columns, state: { globalFilter: searchQuery, sorting }, onSortingChange, onGlobalFilterChange (no-op if search is controlled by parent).getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel().globalFilterFn: use fuzzyFilter from @/lib/utils (uses rankItem from @tanstack/match-sorter-utils), or a custom FilterFn<T> that searches over row fields and calls rankItem/addMeta.initialState: { pagination: { pageSize: 10 } }, autoResetPageIndex: true.const table = useReactTable({
data: items ?? [],
columns,
state: { globalFilter: searchQuery, sorting },
onGlobalFilterChange: () => {},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
globalFilterFn: fuzzyFilter,
initialState: { pagination: { pageSize: 10 } },
autoResetPageIndex: true
});
table.getState().pagination.pageIndex + 1 (1-based), table.getPageCount(), (page) => table.setPageIndex(page - 1), table.getFilteredRowModel().rows.length, table.getState().pagination.pageSize.data, pagination: { total, page, size, pages }, sorting, onSortingChange, onPageChange.state: { sorting }, onSortingChange, getCoreRowModel: getCoreRowModel(), manualSorting: true, manualPagination: true, pageCount: pagination.pages. Do not use filtered/sorted/pagination row models.sort_by) in a small helper and use it when calling the API; pass 1-based page and page size from pagination to the API.table: Table<TData> (from @tanstack/react-table).Table, TableHeader/TableBody, TableRow/TableHead/TableCell.flexRender(header.column.columnDef.header, header.getContext()) and flexRender(cell.column.columnDef.cell, cell.getContext()).(header.column.columnDef.meta as { className?: string })?.className to TableHead, and same for TableCell.header.column.getCanSort(), header.column.getToggleSortingHandler(), and show sort direction (e.g. arrow icons).colSpan={table.getAllColumns().length} and "No results."currentPage (1-based), totalPages, onPageChange: (page: number) => void, optional showInfo, totalItems, pageSize, className.totalPages <= 1, return null. Otherwise render "Showing X to Y of Z" when showInfo and totalItems/pageSize are set, plus prev/next and page numbers.When the default fuzzyFilter (single column value) is not enough, define a FilterFn<T> that builds a searchable string from row.original (including nested fields), then:
const itemRank = rankItem(searchableText, searchValue);
addMeta({ itemRank });
return itemRank.passed;
Use this as globalFilterFn in useReactTable.
table-columns.tsx with ColumnDef<T>[], handlers passed as args, useMemo in view when deps include handlers.globalFilterFn, autoResetPageIndex.manualSorting + manualPagination, pageCount, no client row models for filtering/sorting/pagination.table and uses flexRender and meta.className.format(new Date(date), "MMM dd, yyyy")).reference/src/components/common/ and reference/src/lib/utils.ts.reference/src/components/features/bookings/BookingTableView.tsx and mapColumnIdToSortBy.