| name | tanstack-table-guide |
| description | Guide for using TanStack Table for building powerful data tables with sorting, filtering, pagination, and row selection. Use when implementing data tables, grids, or any tabular data display. Apply when the user asks about TanStack Table, data tables, table sorting, filtering, pagination, or row selection. |
| keywords | tanstack-table, data-table, table, sorting, filtering, pagination, row-selection, useReactTable, columns |
TanStack Table Guide
Overview
TanStack Table is a headless UI library for building powerful, flexible, and performant data tables and datagrids. It provides the table logic without enforcing any UI, making it perfect for custom designs.
Key Features
- 🎨 Headless - No UI, full control over markup and styling
- ⚡ High Performance - Handles large datasets efficiently
- 🔧 Highly Flexible - Extensive plugin system
- 📦 Type-Safe - Full TypeScript support
- 🎯 Framework Agnostic - Works with React, Vue, Solid, etc.
Basic Setup
Installation
pnpm add @tanstack/react-table
Basic Table
import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table'
import type { ColumnDef } from '@tanstack/react-table'
interface User {
id: string
name: string
email: string
age: number
}
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'age',
header: 'Age',
},
]
function UsersTable() {
const [data, setData] = useState<User[]>([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</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>
)
}
Column Definitions
Accessor Types
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorFn: (row) => `${row.firstName} ${row.lastName}`,
id: 'fullName',
header: 'Full Name',
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ getValue }) => {
const status = getValue<string>()
return (
<span className={status === 'active' ? 'text-green-500' : 'text-red-500'}>
{status}
</span>
)
},
},
]
Header Components
{
accessorKey: 'name',
header: ({ column }) => (
<div className="flex items-center gap-2">
Name
{column.getIsSorted() && (
<span>{column.getIsSorted() === 'asc' ? '🔼' : '🔽'}</span>
)}
</div>
),
}
Sorting
Enable Sorting
import { getSortedRowModel } from '@tanstack/react-table'
const [sorting, setSorting] = useState<SortingState>([])
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
Sortable Column Header
{
accessorKey: 'name',
header: ({ column }) => (
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="flex items-center gap-2"
>
Name
{column.getIsSorted() === 'asc' && '🔼'}
{column.getIsSorted() === 'desc' && '🔽'}
</button>
),
}
Custom Sort Function
{
accessorKey: 'status',
header: 'Status',
sortingFn: (rowA, rowB, columnId) => {
const statusOrder = ['pending', 'active', 'completed']
const statusA = rowA.getValue(columnId)
const statusB = rowB.getValue(columnId)
return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB)
},
}
Filtering
Column Filters
import { getFilteredRowModel } from '@tanstack/react-table'
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const table = useReactTable({
data,
columns,
state: {
columnFilters,
},
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
})
Filter Input
function Filter({ column }: { column: Column<any> }) {
const columnFilterValue = column.getFilterValue()
return (
<input
type="text"
value={(columnFilterValue ?? '') as string}
onChange={(e) => column.setFilterValue(e.target.value)}
placeholder={`Search...`}
className="w-full border rounded px-2 py-1"
/>
)
}
Global Filter
const [globalFilter, setGlobalFilter] = useState('')
const table = useReactTable({
data,
columns,
state: {
globalFilter,
},
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: 'includesString',
})
Custom Filter Function
{
accessorKey: 'age',
header: 'Age',
filterFn: (row, columnId, filterValue) => {
const age = row.getValue(columnId) as number
const [min, max] = filterValue as [number, number]
return age >= min && age <= max
},
}
Pagination
Enable Pagination
import { getPaginationRowModel } from '@tanstack/react-table'
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
})
Pagination Controls
function PaginationControls({ table }: { table: Table<any> }) {
return (
<div className="flex items-center gap-2">
<button
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
{'<<'}
</button>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{'<'}
</button>
<span>
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</span>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{'>'}
</button>
<button
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
{'>>'}
</button>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
)
}
Server-Side Pagination
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
})
const { data, isLoading } = useQuery({
queryKey: ['users', pagination],
queryFn: () => fetchUsers(pagination),
})
const table = useReactTable({
data: data?.users ?? [],
columns,
pageCount: data?.pageCount ?? -1,
state: {
pagination,
},
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
})
Row Selection
Enable Row Selection
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const table = useReactTable({
data,
columns,
state: {
rowSelection,
},
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
enableRowSelection: true,
})
Selection Column
const columns: ColumnDef<User>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
]
Get Selected Rows
const selectedRows = table.getSelectedRowModel().rows
const selectedData = selectedRows.map(row => row.original)
console.log('Selected:', selectedData)
Column Visibility
Manage Visibility
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const table = useReactTable({
data,
columns,
state: {
columnVisibility,
},
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
})
Toggle Visibility UI
function ColumnVisibilityMenu({ table }: { table: Table<any> }) {
return (
<div>
{table.getAllColumns().map(column => (
<label key={column.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={column.getIsVisible()}
disabled={!column.getCanHide()}
onChange={column.getToggleVisibilityHandler()}
/>
{column.columnDef.header}
</label>
))}
</div>
)
}
Best Practices
- Use TypeScript - Define clear types for data and columns
- Memoize columns - Use
useMemo to prevent unnecessary re-renders
- Server-side operations - For large datasets, handle sorting/filtering/pagination on the server
- Controlled state - Hoist state for easy access and integration with other features
- Custom components - Create reusable cell/header components
- Performance - Use virtualization for very large tables (see TanStack Virtual)
For detailed patterns and advanced features, see: