Skip to content
Dashboard

A guide to TanStack Table (formerly React Table)

Link to headingWhat is TanStack Table?

Link to headingWhat headless means in practice

import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'
type User = { id: string; name: string; email: string }
const columnHelper = createColumnHelper<User>()
const columns = [
columnHelper.accessor('name', { header: 'Name' }),
columnHelper.accessor('email', { header: 'Email' }),
]
function UserTable({ data }: { data: 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}>
{header.isPlaceholder
? null
: 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>
)
}

Link to headingColumn definitions and type safety

columnHelper.accessor('firstName', {
cell: (info) => info.getValue(),
})
// OR as a plain ColumnDef
{ accessorKey: 'firstName' }

columnHelper.accessor(
(row) => `${row.firstName} ${row.lastName}`,
{
id: 'fullName',
}
)
// OR
{ id: 'fullName', accessorFn: (row) => `${row.firstName} ${row.lastName}` }

columnHelper.accessor('firstName', {
cell: (props) => (
<span>
{`${props.row.original.id} - ${props.getValue()}`}
</span>
),
})

Link to headingThe table instance API

Link to headingThe feature surface, one by one

Link to headingSorting and multi-sort

const [sorting, setSorting] = React.useState<SortingState>([])
const table = useReactTable({
data,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
// enableMultiSort: false -- shift-click multi-sort is on by default
})

Link to headingGlobal filter and column filters

const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const table = useReactTable({
data,
columns,
state: { columnFilters },
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
})
// In a column header: call column.setFilterValue(newValue)

Link to headingPagination, client and server

const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = useReactTable({
data,
columns,
state: { pagination },
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
// pageCount and rowCount not needed for client-side pagination
})

Link to headingRow selection and expansion

const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data,
columns,
state: { rowSelection },
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
})

{
id: 'select',
header: ({ table }) => <input type="checkbox" onChange={table.getToggleAllRowsSelectedHandler()} />,
cell: ({ row }) => <input type="checkbox" checked={row.getIsSelected()} onChange={row.getToggleSelectedHandler()} />,
}

const [expanded, setExpanded] = React.useState<ExpandedState>({})
const table = useReactTable({
data,
columns,
state: { expanded },
onExpandedChange: setExpanded,
getSubRows: (row) => row.subRows, // teach the table where sub-rows live
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
})

Link to headingVisibility, ordering, sizing, pinning

// Visibility
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
// call column.getToggleVisibilityHandler() per column
// Ordering
const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>([])
// call table.setColumnOrder(['col1', 'col2', ...])
// Resizing
// Set columnResizeMode: 'onChange' | 'onEnd' on the table
// header.getResizeHandler() returns onMouseDown / onTouchStart handler
// Pinning
const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({})
// column.pin('left') | column.pin('right') | column.pin(false)

Link to headingClient-side vs server-side data

Link to headingVirtualization with TanStack Virtual

import { useVirtualizer } from '@tanstack/react-virtual'
import { flexRender, type Table } from '@tanstack/react-table'
function TableBody({
table,
tableContainerRef,
}: {
table: Table<Person>
tableContainerRef: React.RefObject<HTMLDivElement>
}) {
const { rows } = table.getRowModel()
const rowVirtualizer = useVirtualizer({
count: rows.length, // total rows from the table's row model
estimateSize: () => 33, // estimated row height for scrollbar accuracy
getScrollElement: () => tableContainerRef.current,
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? (element) => element?.getBoundingClientRect().height
: undefined,
overscan: 5,
})
return (
<tbody
style={{
display: 'grid',
height: `${rowVirtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<tr
data-index={virtualRow.index}
ref={(node) => rowVirtualizer.measureElement(node)}
key={row.id}
style={{
display: 'flex',
position: 'absolute',
transform: `translateY(${virtualRow.start}px)`,
width: '100%',
}}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} style={{ display: 'flex', width: cell.column.getSize() }}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
})}
</tbody>
)
}

Link to headingThe shadcn/ui Data Table pattern

'use client'
import { ColumnDef } from '@tanstack/react-table'
import { Button } from '@/components/ui/button'
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: 'email',
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Email
</Button>
),
},
]

Link to headingPairing with TanStack Query

import { useQuery, keepPreviousData } from '@tanstack/react-query'
import {
useReactTable,
getCoreRowModel,
type PaginationState,
type ColumnDef,
} from '@tanstack/react-table'
// columns: ColumnDef<Person>[] is defined elsewhere in the module
function DataTable() {
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const dataQuery = useQuery<{ rows: Person[]; rowCount: number }>({
queryKey: ['data', pagination],
queryFn: () => fetchData(pagination),
placeholderData: keepPreviousData, // prevents 0-row flash on page turn
})
const defaultData = React.useMemo(() => [], [])
const table = useReactTable({
data: dataQuery.data?.rows ?? defaultData,
columns,
rowCount: dataQuery.data?.rowCount,
state: { pagination },
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
})
// render table and pagination controls
}

Link to headingWhen TanStack Table is the right choice

Link to headingA starter checklist

Link to headingFrequently asked questions

Link to headingWhat version does this guide cover?

Link to headingShould I wait for v9?

Link to headingDoes it support Vue, Solid, and Svelte?

Link to headingHow do I reset pageIndex on filter change?

Link to headingWhy do row selections clear when I sort or filter?

Ready to deploy?