Data Table
A full-featured data table with sorting, filtering, pagination, and row selection.
Preview
A data table with:
- Column sorting (single and multi)
- Global search filter
- Pagination with page size selector
- Row selection with bulk actions
- Loading and empty states
- TypeScript generics for type safety
Dependencies
npm install @tanstack/react-table @tanstack/react-query
npx shadcn-ui@latest add table button input checkbox selectCode
import { useState } from 'react';
import {
flexRender,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
useReactTable,
type ColumnDef,
type SortingState,
type ColumnFiltersState,
type RowSelectionState,
} from '@tanstack/react-table';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
isLoading?: boolean;
onRowSelectionChange?: (selectedRows: TData[]) => void;
searchPlaceholder?: string;
}
export function DataTable<TData, TValue>({
columns,
data,
isLoading = false,
onRowSelectionChange,
searchPlaceholder = 'Search...',
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const table = useReactTable({
data,
columns,
state: {
sorting,
columnFilters,
globalFilter,
rowSelection,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onRowSelectionChange: (updater) => {
const newSelection = typeof updater === 'function'
? updater(rowSelection)
: updater;
setRowSelection(newSelection);
if (onRowSelectionChange) {
const selectedRows = Object.keys(newSelection)
.filter(key => newSelection[key])
.map(key => data[parseInt(key)]);
onRowSelectionChange(selectedRows);
}
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
const selectedCount = Object.keys(rowSelection).filter(k => rowSelection[k]).length;
return (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex items-center justify-between gap-4">
<Input
placeholder={searchPlaceholder}
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className="max-w-sm"
/>
{selectedCount > 0 && (
<span className="text-sm text-muted-foreground">
{selectedCount} row(s) selected
</span>
)}
</div>
{/* Table */}
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : (
<div
className={
header.column.getCanSort()
? 'flex cursor-pointer select-none items-center gap-2'
: ''
}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && (
<span className="text-muted-foreground">
{header.column.getIsSorted() === 'asc' ? (
<ChevronUp className="h-4 w-4" />
) : header.column.getIsSorted() === 'desc' ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronsUpDown className="h-4 w-4" />
)}
</span>
)}
</div>
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading...
</TableCell>
</TableRow>
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Rows per page</span>
<Select
value={String(table.getState().pagination.pageSize)}
onValueChange={(value) => table.setPageSize(Number(value))}
>
<SelectTrigger className="w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[10, 20, 30, 50].map((size) => (
<SelectItem key={size} value={String(size)}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</span>
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
</div>
);
}Features
Sorting
Click column headers to sort. Supports multi-column sorting with Shift+Click.
Filtering
- Global search across all columns
- Per-column filtering (add column filter inputs)
- Debounced search for performance
Pagination
- Client-side pagination included
- Easy to adapt for server-side pagination
- Configurable page sizes
Row Selection
- Select individual rows or all rows
- Callback when selection changes
- Use for bulk actions
Server-Side Pagination
For large datasets, use server-side pagination:
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const { data } = useQuery({
queryKey: ['users', pagination],
queryFn: () => fetchUsers(pagination),
});
const table = useReactTable({
data: data?.items ?? [],
pageCount: data?.totalPages ?? -1,
state: { pagination },
onPaginationChange: setPagination,
manualPagination: true,
// ... other options
});Related
- Search with Filters - Advanced filtering
- Infinite List - Infinite scroll pattern