Skip to content
Dashboard

A guide to TanStack Table (formerly React Table)

Copy link to headingWhat is TanStack Table?

Copy 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>
)
}

Copy 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>
),
})

Copy link to headingThe table instance API

Copy link to headingThe feature surface, one by one

Copy 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
})

Copy 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)

Copy 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
})

Copy 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(),
})

Copy 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)

Copy link to headingClient-side vs server-side data

Copy 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>
)
}

Copy 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>
),
},
]

Copy 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
}

Copy link to headingWhen TanStack Table is the right choice

Copy link to headingA starter checklist

Copy link to headingFrequently asked questions

Copy link to headingWhat version does this guide cover?

Copy link to headingShould I wait for v9?

Copy link to headingDoes it support Vue, Solid, and Svelte?

Copy link to headingHow do I reset pageIndex on filter change?

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

Ready to deploy?