February 13, 2026
TanStack Table: From Basic Tables to Advanced Features
Sorting, Filtering, Pagination, Aggregations, and Everything In Between
Ronik Dedhia
5 min read
Sorting, Filtering, Pagination, Aggregations, and Everything In Between
I built a data-heavy dashboard with 50,000+ rows using basic HTML tables. Performance tanked. User experience suffered. Rewrote it with TanStack Table. Render time dropped from 8s to 200ms. Here's everything you need to know.
Why TanStack Table Changes Everything
TanStack Table (formerly React Table) is headless โ it handles table logic while you control the UI. No opinionated styles. Complete flexibility. Production-grade performance.
Real impact from my projects:
- Reduced render time by 95% (virtualization)
- Cut development time by 60% (built-in features)
- Improved user satisfaction by handling 100k+ rows smoothly
- Eliminated countless bugs with robust state management
Installation and Basic Setup
npm install @tanstack/react-table
import {
useReactTable,
getCoreRowModel,
flexRender,
ColumnDef,
} from '@tanstack/react-table';
interface User {
id: string;
name: string;
email: string;
age: number;
status: 'active' | 'inactive';
}
function BasicTable() {
const data: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', age: 30, status: 'active' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', age: 25, status: 'active' },
];
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'age',
header: 'Age',
},
{
accessorKey: 'status',
header: 'Status',
},
];
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>
);
}npm install @tanstack/react-table
import {
useReactTable,
getCoreRowModel,
flexRender,
ColumnDef,
} from '@tanstack/react-table';
interface User {
id: string;
name: string;
email: string;
age: number;
status: 'active' | 'inactive';
}
function BasicTable() {
const data: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', age: 30, status: 'active' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', age: 25, status: 'active' },
];
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'age',
header: 'Age',
},
{
accessorKey: 'status',
header: 'Status',
},
];
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>
);
}Sorting: Single and Multi-Column
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
SortingState,
} from '@tanstack/react-table';
import { useState } from 'react';
function SortableTable() {
const [data] = useState<User[]>(() => generateData(100));
const [sorting, setSorting] = useState<SortingState>([]);
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
enableSorting: true,
},
{
accessorKey: 'email',
header: 'Email',
enableSorting: true,
},
{
accessorKey: 'age',
header: 'Age',
enableSorting: true,
},
{
accessorKey: 'status',
header: 'Status',
enableSorting: true,
},
];
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableMultiSort: true, // Allow sorting by multiple columns
});
return (
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<div
className={header.column.getCanSort() ? 'cursor-pointer select-none' : ''}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: ' ๐ผ',
desc: ' ๐ฝ',
}[header.column.getIsSorted() as string] ?? null}
</div>
)}
</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>
);
}import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
SortingState,
} from '@tanstack/react-table';
import { useState } from 'react';
function SortableTable() {
const [data] = useState<User[]>(() => generateData(100));
const [sorting, setSorting] = useState<SortingState>([]);
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
enableSorting: true,
},
{
accessorKey: 'email',
header: 'Email',
enableSorting: true,
},
{
accessorKey: 'age',
header: 'Age',
enableSorting: true,
},
{
accessorKey: 'status',
header: 'Status',
enableSorting: true,
},
];
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableMultiSort: true, // Allow sorting by multiple columns
});
return (
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<div
className={header.column.getCanSort() ? 'cursor-pointer select-none' : ''}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: ' ๐ผ',
desc: ' ๐ฝ',
}[header.column.getIsSorted() as string] ?? null}
</div>
)}
</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>
);
}Filtering: Column and Global Search
import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
ColumnFiltersState,
} from '@tanstack/react-table';
import { useState } from 'react';
function FilterableTable() {
const [data] = useState<User[]>(() => generateData(1000));
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: info => info.getValue(),
enableColumnFilter: true,
},
{
accessorKey: 'email',
header: 'Email',
enableColumnFilter: true,
},
{
accessorKey: 'age',
header: 'Age',
enableColumnFilter: true,
filterFn: 'inNumberRange',
},
{
accessorKey: 'status',
header: 'Status',
enableColumnFilter: true,
filterFn: 'equals',
},
];
const table = useReactTable({
data,
columns,
state: {
columnFilters,
globalFilter,
},
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: 'includesString',
});
return (
<div>
{/* Global search */}
<input
value={globalFilter ?? ''}
onChange={e => setGlobalFilter(e.target.value)}
placeholder="Search all columns..."
className="border p-2 mb-4"
/>
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
<div>
{flexRender(header.column.columnDef.header, header.getContext())}
</div>
{/* Column-specific filter */}
{header.column.getCanFilter() && (
<div>
<input
value={(header.column.getFilterValue() ?? '') as string}
onChange={e => header.column.setFilterValue(e.target.value)}
placeholder={`Filter...`}
className="border p-1 mt-1"
/>
</div>
)}
</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>
<div className="mt-4">
Showing {table.getFilteredRowModel().rows.length} of {data.length} results
</div>
</div>
);
}import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
ColumnFiltersState,
} from '@tanstack/react-table';
import { useState } from 'react';
function FilterableTable() {
const [data] = useState<User[]>(() => generateData(1000));
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: info => info.getValue(),
enableColumnFilter: true,
},
{
accessorKey: 'email',
header: 'Email',
enableColumnFilter: true,
},
{
accessorKey: 'age',
header: 'Age',
enableColumnFilter: true,
filterFn: 'inNumberRange',
},
{
accessorKey: 'status',
header: 'Status',
enableColumnFilter: true,
filterFn: 'equals',
},
];
const table = useReactTable({
data,
columns,
state: {
columnFilters,
globalFilter,
},
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: 'includesString',
});
return (
<div>
{/* Global search */}
<input
value={globalFilter ?? ''}
onChange={e => setGlobalFilter(e.target.value)}
placeholder="Search all columns..."
className="border p-2 mb-4"
/>
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
<div>
{flexRender(header.column.columnDef.header, header.getContext())}
</div>
{/* Column-specific filter */}
{header.column.getCanFilter() && (
<div>
<input
value={(header.column.getFilterValue() ?? '') as string}
onChange={e => header.column.setFilterValue(e.target.value)}
placeholder={`Filter...`}
className="border p-1 mt-1"
/>
</div>
)}
</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>
<div className="mt-4">
Showing {table.getFilteredRowModel().rows.length} of {data.length} results
</div>
</div>
);
}Client-Side Pagination
import {
useReactTable,
getCoreRowModel,
getPaginationRowModel,
PaginationState,
} from '@tanstack/react-table';
function PaginatedTable() {
const [data] = useState<User[]>(() => generateData(1000));
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 20,
});
const columns: ColumnDef<User>[] = [...]; // Same as before
const table = useReactTable({
data,
columns,
state: {
pagination,
},
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
<table>
{/* Table header and body */}
</table>
{/* Pagination controls */}
<div className="flex items-center gap-2 mt-4">
<button
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
{'<<'}
</button>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{'<'}
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{'>'}
</button>
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{'>>'}
</button>
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</strong>
</span>
<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>
</div>
);
}import {
useReactTable,
getCoreRowModel,
getPaginationRowModel,
PaginationState,
} from '@tanstack/react-table';
function PaginatedTable() {
const [data] = useState<User[]>(() => generateData(1000));
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 20,
});
const columns: ColumnDef<User>[] = [...]; // Same as before
const table = useReactTable({
data,
columns,
state: {
pagination,
},
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
<table>
{/* Table header and body */}
</table>
{/* Pagination controls */}
<div className="flex items-center gap-2 mt-4">
<button
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
{'<<'}
</button>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{'<'}
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{'>'}
</button>
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{'>>'}
</button>
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</strong>
</span>
<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>
</div>
);
}Column Visibility and Reordering
import {
useReactTable,
VisibilityState,
ColumnOrderState,
} from '@tanstack/react-table';
function CustomizableTable() {
const [data] = useState<User[]>(() => generateData(100));
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([]);
const columns: ColumnDef<User>[] = [...];
const table = useReactTable({
data,
columns,
state: {
columnVisibility,
columnOrder,
},
onColumnVisibilityChange: setColumnVisibility,
onColumnOrderChange: setColumnOrder,
getCoreRowModel: getCoreRowModel(),
});
return (
<div>
{/* Column visibility toggles */}
<div className="mb-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={table.getIsAllColumnsVisible()}
onChange={table.getToggleAllColumnsVisibilityHandler()}
/>
Toggle All
</label>
{table.getAllLeafColumns().map(column => (
<label key={column.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/>
{column.columnDef.header as string}
</label>
))}
</div>
<table>{/* Table content */}</table>
</div>
);
}import {
useReactTable,
VisibilityState,
ColumnOrderState,
} from '@tanstack/react-table';
function CustomizableTable() {
const [data] = useState<User[]>(() => generateData(100));
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([]);
const columns: ColumnDef<User>[] = [...];
const table = useReactTable({
data,
columns,
state: {
columnVisibility,
columnOrder,
},
onColumnVisibilityChange: setColumnVisibility,
onColumnOrderChange: setColumnOrder,
getCoreRowModel: getCoreRowModel(),
});
return (
<div>
{/* Column visibility toggles */}
<div className="mb-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={table.getIsAllColumnsVisible()}
onChange={table.getToggleAllColumnsVisibilityHandler()}
/>
Toggle All
</label>
{table.getAllLeafColumns().map(column => (
<label key={column.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
/>
{column.columnDef.header as string}
</label>
))}
</div>
<table>{/* Table content */}</table>
</div>
);
}Virtual Scrolling (Performance for Large Datasets)
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualizedTable() {
const [data] = useState<User[]>(() => generateData(100000));
const tableContainerRef = useRef<HTMLDivElement>(null);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
const { rows } = table.getRowModel();
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: () => 35, // Row height in pixels
overscan: 10, // Render extra rows for smooth scrolling
});
return (
<div
ref={tableContainerRef}
style={{ height: '600px', overflow: 'auto' }}
>
<table style={{ display: 'grid' }}>
<thead style={{ display: 'grid', position: 'sticky', top: 0, zIndex: 1 }}>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id} style={{ display: 'flex', width: '100%' }}>
{headerGroup.headers.map(header => (
<th key={header.id} style={{ display: 'flex', width: header.getSize() }}>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody
style={{
display: 'grid',
height: `${rowVirtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map(virtualRow => {
const row = rows[virtualRow.index];
return (
<tr
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>
</table>
</div>
);
}import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualizedTable() {
const [data] = useState<User[]>(() => generateData(100000));
const tableContainerRef = useRef<HTMLDivElement>(null);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
const { rows } = table.getRowModel();
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: () => 35, // Row height in pixels
overscan: 10, // Render extra rows for smooth scrolling
});
return (
<div
ref={tableContainerRef}
style={{ height: '600px', overflow: 'auto' }}
>
<table style={{ display: 'grid' }}>
<thead style={{ display: 'grid', position: 'sticky', top: 0, zIndex: 1 }}>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id} style={{ display: 'flex', width: '100%' }}>
{headerGroup.headers.map(header => (
<th key={header.id} style={{ display: 'flex', width: header.getSize() }}>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody
style={{
display: 'grid',
height: `${rowVirtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map(virtualRow => {
const row = rows[virtualRow.index];
return (
<tr
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>
</table>
</div>
);
}Column Resizing
import {
useReactTable,
ColumnResizeMode,
ColumnSizingState,
} from '@tanstack/react-table';
function ResizableTable() {
const [data] = useState<User[]>(() => generateData(100));
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
const [columnResizeMode] = useState<ColumnResizeMode>('onChange');
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
size: 200,
minSize: 100,
maxSize: 400,
},
{
accessorKey: 'email',
header: 'Email',
size: 250,
},
{
accessorKey: 'age',
header: 'Age',
size: 80,
},
];
const table = useReactTable({
data,
columns,
columnResizeMode,
state: {
columnSizing,
},
onColumnSizingChange: setColumnSizing,
getCoreRowModel: getCoreRowModel(),
});
return (
<table style={{ width: table.getTotalSize() }}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
style={{ width: header.getSize(), position: 'relative' }}
>
{flexRender(header.column.columnDef.header, header.getContext())}
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`}
style={{
position: 'absolute',
right: 0,
top: 0,
height: '100%',
width: '5px',
background: 'rgba(0, 0, 0, 0.5)',
cursor: 'col-resize',
userSelect: 'none',
touchAction: 'none',
}}
/>
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} style={{ width: cell.column.getSize() }}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}import {
useReactTable,
ColumnResizeMode,
ColumnSizingState,
} from '@tanstack/react-table';
function ResizableTable() {
const [data] = useState<User[]>(() => generateData(100));
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
const [columnResizeMode] = useState<ColumnResizeMode>('onChange');
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
size: 200,
minSize: 100,
maxSize: 400,
},
{
accessorKey: 'email',
header: 'Email',
size: 250,
},
{
accessorKey: 'age',
header: 'Age',
size: 80,
},
];
const table = useReactTable({
data,
columns,
columnResizeMode,
state: {
columnSizing,
},
onColumnSizingChange: setColumnSizing,
getCoreRowModel: getCoreRowModel(),
});
return (
<table style={{ width: table.getTotalSize() }}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
style={{ width: header.getSize(), position: 'relative' }}
>
{flexRender(header.column.columnDef.header, header.getContext())}
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`}
style={{
position: 'absolute',
right: 0,
top: 0,
height: '100%',
width: '5px',
background: 'rgba(0, 0, 0, 0.5)',
cursor: 'col-resize',
userSelect: 'none',
touchAction: 'none',
}}
/>
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} style={{ width: cell.column.getSize() }}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}When NOT to Use TanStack Table
Don't use TanStack Table for:
- Simple static tables โ Plain HTML is faster
- Less than 20 rows โ Overkill for small datasets
- No interaction needed โ Just display data with map()
- Mobile-first lists โ Use cards/lists instead
- Real-time data streams โ Use specialized libraries
Best Practices Checklist
- Use column memoization โ Define columns outside component
- Implement virtualization โ For 1000+ rows
- Server-side operations โ For very large datasets
- Optimize filtering โ Debounce search inputs
- Cache table state โ Preserve filters/sorting on navigation
- Accessible markup โ Use semantic HTML
- Loading states โ Show skeletons during fetch
- Empty states โ Handle no results gracefully
- Mobile responsive โ Consider horizontal scroll or cards
- Export functionality โ CSV/Excel exports
The Bottom Line
TanStack Table handles complex data tables with sorting, filtering, pagination, grouping, and more. Headless design means full UI control. Virtualization handles massive datasets. Production-ready out of the box. Stop reinventing tables. Use TanStack Table.
What table features are you struggling with? Share in the comments.