This boilerplate includes powerful data grids powered by TanStack Table v8 with features like sorting, filtering, editing, bulk operations, and more.
- Overview
- TanStack Table Features
- Built-in Grid Examples
- Basic Usage
- Advanced Features
- Styling and Theming
- Performance Optimization
- Best Practices
TanStack Table (formerly React Table) is a headless UI library for building powerful tables and data grids. It provides:
- Headless architecture: Full control over rendering and styling
- TypeScript-first: Fully type-safe APIs
- Framework agnostic: Works with React, Vue, Solid, Svelte
- Feature-rich: Sorting, filtering, pagination, grouping, selection, and more
- Virtualization support: Handle thousands of rows efficiently
- Tree data support: Nested rows and expandable sections
- Flexibility: Complete control over UI and behavior
- Performance: Optimized for large datasets
- Type Safety: Full TypeScript support with type inference
- Extensibility: Plugin-based architecture
- Modern: Built for modern React with hooks
The boilerplate includes examples of the following features:
- Single column sorting: Click column header to sort
- Multi-column sorting: Hold Shift and click multiple headers
- Custom sort functions: Define how data should be sorted
- Column filters: Filter individual columns
- Global filter: Search across all columns
- Custom filter functions: Define complex filtering logic
- Single row selection: Click to select one row
- Multi-row selection: Checkboxes for selecting multiple rows
- Select all: Checkbox in header to select all rows
- Programmatic selection: Control selection via state
- Inline editing: Click cell to edit
- Validation: Validate input before saving
- Controlled inputs: React-controlled form inputs
- Page size control: Choose rows per page (10, 25, 50, 100)
- Page navigation: First, previous, next, last page buttons
- Page info: Display current page and total pages
- Nested data: Show child rows
- Expandable sections: Click to expand/collapse
- Indentation: Visual hierarchy
- Batch actions: Perform actions on multiple selected rows
- Delete selected: Remove multiple rows at once
- Export selected: Export selected rows to CSV/JSON
- Show/hide columns: Toggle column visibility
- Column reordering: Drag and drop columns (with plugin)
- Column pinning: Pin columns to left/right (with plugin)
The boilerplate includes several grid examples at /grids:
Route: /grids → "Crud Records" tab
Features:
- Live data from API (Prisma)
- Tenant-scoped data
- Read-only grid
- Sorting and filtering
- Pagination
Use Case: Display database records in a table
Route: /grids → "Global Crud Records" tab
Features:
- Live data from API (Prisma)
- Shared across all tenants
- Read-only grid
- Sorting and filtering
Use Case: Display global/shared data
Route: /grids → "Editable People" tab
Features:
- In-memory demo data
- Inline cell editing
- Add/remove rows
- Unsaved changes indicator
- Form validation
Use Case: Editable data grids with validation
Route: /grids → "Batch Operations" tab
Features:
- Multi-row selection
- Bulk delete
- Export to JSON
- Change status in bulk
- Selected count display
Use Case: Perform actions on multiple rows
Route: /grids → "Tenants" tab
Features:
- Grouped rows (by plan tier)
- Expandable sub-rows
- Nested data display
- Custom cell renderers
Use Case: Hierarchical data with grouping
Route: /grids → "Metrics" tab
Features:
- Read-only analytics grid
- Custom cell formatting (percentages, currency)
- Color-coded values
- Trend indicators
Use Case: Display analytics and metrics
Route: /grids → "Tasks" tab
Features:
- Rich cell renderers (tags, avatars, priority icons)
- Status badges
- Due date highlighting
- Expandable details
Use Case: Project management or task tracking
import {
useReactTable,
getCoreRowModel,
flexRender,
} from '@tanstack/react-table';
import { useState } from 'react';
type Person = {
id: string;
firstName: string;
lastName: string;
email: string;
};
function SimpleTable() {
const [data] = useState<Person[]>([
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
{ id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' },
]);
const columns = [
{
accessorKey: 'firstName',
header: 'First Name',
},
{
accessorKey: 'lastName',
header: 'Last Name',
},
{
accessorKey: 'email',
header: 'Email',
},
];
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>
);
}Enable sorting on columns:
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
SortingState,
} from '@tanstack/react-table';
import { useState } from 'react';
function SortableTable() {
const [data] = useState([...]);
const [sorting, setSorting] = useState<SortingState>([]);
const columns = [
{
accessorKey: 'name',
header: 'Name',
enableSorting: true,
},
{
accessorKey: 'age',
header: 'Age',
enableSorting: true,
},
];
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
style={{ cursor: 'pointer' }}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === 'asc' ? ' 🔼' : ''}
{header.column.getIsSorted() === 'desc' ? ' 🔽' : ''}
</th>
))}
</tr>
))}
</thead>
{/* ... body */}
</table>
);
}Add checkboxes for row selection:
import {
useReactTable,
getCoreRowModel,
RowSelectionState,
} from '@tanstack/react-table';
import { useState } from 'react';
function SelectableTable() {
const [data] = useState([...]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const columns = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
accessorKey: 'name',
header: 'Name',
},
];
const table = useReactTable({
data,
columns,
state: {
rowSelection,
},
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
});
const selectedCount = Object.keys(rowSelection).length;
return (
<div>
{selectedCount > 0 && (
<div>
<span>{selectedCount} selected</span>
<button onClick={() => {
// Handle bulk action
const selectedRows = table.getSelectedRowModel().rows;
console.log('Selected:', selectedRows);
}}>
Delete Selected
</button>
</div>
)}
<table>{/* ... */}</table>
</div>
);
}Make cells editable:
function EditableCell({ getValue, row, column, table }) {
const initialValue = getValue();
const [value, setValue] = useState(initialValue);
const onBlur = () => {
table.options.meta?.updateData(row.index, column.id, value);
};
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
/>
);
}
function EditableTable() {
const [data, setData] = useState([...]);
const columns = [
{
accessorKey: 'firstName',
header: 'First Name',
cell: EditableCell,
},
{
accessorKey: 'lastName',
header: 'Last Name',
cell: EditableCell,
},
];
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
meta: {
updateData: (rowIndex, columnId, value) => {
setData((old) =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
};
}
return row;
})
);
},
},
});
return <table>{/* ... */}</table>;
}Add pagination controls:
import {
useReactTable,
getCoreRowModel,
getPaginationRowModel,
PaginationState,
} from '@tanstack/react-table';
import { useState } from 'react';
function PaginatedTable() {
const [data] = useState([...]);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const table = useReactTable({
data,
columns,
state: {
pagination,
},
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
<table>{/* ... */}</table>
<div>
<button
onClick={() => table.setPageIndex(0)}
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.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{'>>'}
</button>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))}
>
{[10, 25, 50, 100].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
);
}Create rich cell components:
function StatusCell({ getValue }) {
const status = getValue();
const colors = {
active: 'bg-green-100 text-green-800',
inactive: 'bg-gray-100 text-gray-800',
suspended: 'bg-red-100 text-red-800',
};
return (
<span className={`px-2 py-1 rounded ${colors[status]}`}>
{status}
</span>
);
}
function AvatarCell({ row }) {
return (
<div className="flex items-center gap-2">
<img
src={row.original.avatar}
alt={row.original.name}
className="w-8 h-8 rounded-full"
/>
<span>{row.original.name}</span>
</div>
);
}
const columns = [
{
accessorKey: 'name',
header: 'User',
cell: AvatarCell,
},
{
accessorKey: 'status',
header: 'Status',
cell: StatusCell,
},
];Add column and global filters:
import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
ColumnFiltersState,
} from '@tanstack/react-table';
import { useState } from 'react';
function FilterableTable() {
const [data] = useState([...]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: {
columnFilters,
globalFilter,
},
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
return (
<div>
<input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search all columns..."
/>
<table>{/* ... */}</table>
</div>
);
}The boilerplate uses Tailwind CSS for styling. Example styled table:
function StyledTable() {
return (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{table.getHeaderGroups().map((headerGroup) =>
headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))
)}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}The boilerplate includes dark mode support via next-themes:
<table className="dark:bg-gray-800 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900">
<th className="text-gray-500 dark:text-gray-400">
{/* ... */}
</th>
</thead>
<tbody className="bg-white dark:bg-gray-800">
<td className="text-gray-900 dark:text-gray-100">
{/* ... */}
</td>
</tbody>
</table>For large datasets (1000+ rows), use virtualization:
pnpm add @tanstack/react-virtualimport { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualizedTable() {
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: table.getRowModel().rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Row height in pixels
overscan: 10,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = table.getRowModel().rows[virtualRow.index];
return (
<div
key={row.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{/* Render row cells */}
</div>
);
})}
</div>
</div>
);
}Use useMemo for columns:
import { useMemo } from 'react';
function OptimizedTable() {
const columns = useMemo(
() => [
{
accessorKey: 'name',
header: 'Name',
},
// ... other columns
],
[]
);
const table = useReactTable({
data,
columns,
// ...
});
}For very large datasets, implement server-side sorting/filtering/pagination:
function ServerSideTable() {
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const [sorting, setSorting] = useState([]);
const { data, isLoading } = trpc.data.list.useQuery({
page: pagination.pageIndex,
pageSize: pagination.pageSize,
sortBy: sorting[0]?.id,
sortOrder: sorting[0]?.desc ? 'desc' : 'asc',
});
const table = useReactTable({
data: data?.items ?? [],
columns,
pageCount: data?.pageCount ?? -1,
state: {
pagination,
sorting,
},
onPaginationChange: setPagination,
onSortingChange: setSorting,
manualPagination: true,
manualSorting: true,
getCoreRowModel: getCoreRowModel(),
});
if (isLoading) return <div>Loading...</div>;
return <table>{/* ... */}</table>;
}Always type your data:
type User = {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
};
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
];Create reusable table components:
// DataTable.tsx
function DataTable<TData>({
data,
columns,
}: {
data: TData[];
columns: ColumnDef<TData>[];
}) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return <table>{/* ... */}</table>;
}
// Usage
<DataTable data={users} columns={userColumns} />The column helper provides better type inference:
import { createColumnHelper } from '@tanstack/react-table';
const columnHelper = createColumnHelper<User>();
const columns = [
columnHelper.accessor('name', {
header: 'Name',
cell: (info) => info.getValue(), // Fully typed!
}),
columnHelper.accessor('email', {
header: 'Email',
}),
];Show loading indicators:
function DataTable() {
const { data, isLoading } = trpc.data.list.useQuery();
if (isLoading) {
return <div>Loading...</div>;
}
if (!data || data.length === 0) {
return <div>No data available</div>;
}
return <table>{/* ... */}</table>;
}Handle API errors gracefully:
const { data, isLoading, error } = trpc.data.list.useQuery();
if (error) {
return <div>Error: {error.message}</div>;
}