Table Patterns
Build powerful data tables with sorting, filtering, and pagination using TanStack Table.
Overview#
Tables are essential for displaying structured data. This pattern covers:
- Basic table rendering with TanStack Table
- Sortable columns
- Pagination (client and server-side)
- Filtering and search
- Row selection
- Server-side data fetching
Prerequisites#
npm install @tanstack/react-table lucide-reactBasic Table#
A simple table with column definitions and data rendering.
1// components/tables/DataTable.tsx
2'use client'
3
4import {
5 useReactTable,
6 getCoreRowModel,
7 flexRender,
8 ColumnDef
9} from '@tanstack/react-table'
10
11interface Props<T> {
12 data: T[]
13 columns: ColumnDef<T>[]
14}
15
16export function DataTable<T>({ data, columns }: Props<T>) {
17 const table = useReactTable({
18 data,
19 columns,
20 getCoreRowModel: getCoreRowModel()
21 })
22
23 return (
24 <table className="w-full border-collapse">
25 <thead>
26 {table.getHeaderGroups().map(headerGroup => (
27 <tr key={headerGroup.id}>
28 {headerGroup.headers.map(header => (
29 <th key={header.id} className="border-b p-2 text-left">
30 {flexRender(
31 header.column.columnDef.header,
32 header.getContext()
33 )}
34 </th>
35 ))}
36 </tr>
37 ))}
38 </thead>
39 <tbody>
40 {table.getRowModel().rows.map(row => (
41 <tr key={row.id} className="hover:bg-gray-50">
42 {row.getVisibleCells().map(cell => (
43 <td key={cell.id} className="border-b p-2">
44 {flexRender(cell.column.columnDef.cell, cell.getContext())}
45 </td>
46 ))}
47 </tr>
48 ))}
49 </tbody>
50 </table>
51 )
52}
53
54// Usage
55const columns: ColumnDef<User>[] = [
56 { accessorKey: 'name', header: 'Name' },
57 { accessorKey: 'email', header: 'Email' },
58 {
59 accessorKey: 'role',
60 header: 'Role',
61 cell: ({ getValue }) => (
62 <span className="rounded bg-gray-100 px-2 py-1">
63 {getValue() as string}
64 </span>
65 )
66 }
67]
68
69<DataTable data={users} columns={columns} />Sortable Table#
Add sorting capabilities to any column.
1// components/tables/SortableTable.tsx
2'use client'
3
4import {
5 useReactTable,
6 getCoreRowModel,
7 getSortedRowModel,
8 SortingState,
9 flexRender,
10 ColumnDef
11} from '@tanstack/react-table'
12import { useState } from 'react'
13import { ChevronUp, ChevronDown } from 'lucide-react'
14
15export function SortableTable<T>({ data, columns }: Props<T>) {
16 const [sorting, setSorting] = useState<SortingState>([])
17
18 const table = useReactTable({
19 data,
20 columns,
21 state: { sorting },
22 onSortingChange: setSorting,
23 getCoreRowModel: getCoreRowModel(),
24 getSortedRowModel: getSortedRowModel()
25 })
26
27 return (
28 <table className="w-full">
29 <thead>
30 {table.getHeaderGroups().map(headerGroup => (
31 <tr key={headerGroup.id}>
32 {headerGroup.headers.map(header => (
33 <th
34 key={header.id}
35 className="cursor-pointer select-none p-2"
36 onClick={header.column.getToggleSortingHandler()}
37 >
38 <div className="flex items-center gap-1">
39 {flexRender(
40 header.column.columnDef.header,
41 header.getContext()
42 )}
43 {{
44 asc: <ChevronUp className="h-4 w-4" />,
45 desc: <ChevronDown className="h-4 w-4" />
46 }[header.column.getIsSorted() as string] ?? null}
47 </div>
48 </th>
49 ))}
50 </tr>
51 ))}
52 </thead>
53 {/* ... tbody same as basic */}
54 </table>
55 )
56}Paginated Table#
Add pagination controls to handle large datasets.
1// components/tables/PaginatedTable.tsx
2'use client'
3
4import {
5 useReactTable,
6 getCoreRowModel,
7 getPaginationRowModel,
8 PaginationState,
9 flexRender
10} from '@tanstack/react-table'
11import { useState } from 'react'
12
13export function PaginatedTable<T>({ data, columns }: Props<T>) {
14 const [pagination, setPagination] = useState<PaginationState>({
15 pageIndex: 0,
16 pageSize: 10
17 })
18
19 const table = useReactTable({
20 data,
21 columns,
22 state: { pagination },
23 onPaginationChange: setPagination,
24 getCoreRowModel: getCoreRowModel(),
25 getPaginationRowModel: getPaginationRowModel()
26 })
27
28 return (
29 <div>
30 <table className="w-full">
31 {/* ... thead and tbody */}
32 </table>
33
34 <div className="mt-4 flex items-center justify-between">
35 <div>
36 Page {table.getState().pagination.pageIndex + 1} of{' '}
37 {table.getPageCount()}
38 </div>
39
40 <div className="flex gap-2">
41 <button
42 onClick={() => table.previousPage()}
43 disabled={!table.getCanPreviousPage()}
44 >
45 Previous
46 </button>
47 <button
48 onClick={() => table.nextPage()}
49 disabled={!table.getCanNextPage()}
50 >
51 Next
52 </button>
53 </div>
54
55 <select
56 value={pagination.pageSize}
57 onChange={e => table.setPageSize(Number(e.target.value))}
58 >
59 {[10, 20, 50, 100].map(size => (
60 <option key={size} value={size}>
61 Show {size}
62 </option>
63 ))}
64 </select>
65 </div>
66 </div>
67 )
68}Filterable Table#
Add global and column-specific filtering.
1// components/tables/FilterableTable.tsx
2'use client'
3
4import {
5 useReactTable,
6 getCoreRowModel,
7 getFilteredRowModel,
8 ColumnFiltersState,
9 flexRender
10} from '@tanstack/react-table'
11import { useState } from 'react'
12
13export function FilterableTable<T>({ data, columns }: Props<T>) {
14 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
15 const [globalFilter, setGlobalFilter] = useState('')
16
17 const table = useReactTable({
18 data,
19 columns,
20 state: { columnFilters, globalFilter },
21 onColumnFiltersChange: setColumnFilters,
22 onGlobalFilterChange: setGlobalFilter,
23 getCoreRowModel: getCoreRowModel(),
24 getFilteredRowModel: getFilteredRowModel()
25 })
26
27 return (
28 <div>
29 {/* Global search */}
30 <input
31 type="text"
32 placeholder="Search all columns..."
33 value={globalFilter}
34 onChange={e => setGlobalFilter(e.target.value)}
35 className="mb-4 w-full rounded border p-2"
36 />
37
38 <table className="w-full">
39 <thead>
40 {table.getHeaderGroups().map(headerGroup => (
41 <tr key={headerGroup.id}>
42 {headerGroup.headers.map(header => (
43 <th key={header.id} className="p-2">
44 {flexRender(
45 header.column.columnDef.header,
46 header.getContext()
47 )}
48 {header.column.getCanFilter() && (
49 <input
50 type="text"
51 value={(header.column.getFilterValue() ?? '') as string}
52 onChange={e => header.column.setFilterValue(e.target.value)}
53 placeholder="Filter..."
54 className="mt-1 w-full rounded border p-1 text-sm"
55 />
56 )}
57 </th>
58 ))}
59 </tr>
60 ))}
61 </thead>
62 {/* ... tbody */}
63 </table>
64 </div>
65 )
66}Selectable Table#
Enable row selection with checkboxes.
1// components/tables/SelectableTable.tsx
2'use client'
3
4import {
5 useReactTable,
6 getCoreRowModel,
7 RowSelectionState,
8 flexRender,
9 ColumnDef
10} from '@tanstack/react-table'
11import { useState } from 'react'
12
13export function SelectableTable<T>({ data, columns, onSelectionChange }: Props<T> & {
14 onSelectionChange?: (selected: T[]) => void
15}) {
16 const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
17
18 const selectionColumn: ColumnDef<T> = {
19 id: 'select',
20 header: ({ table }) => (
21 <input
22 type="checkbox"
23 checked={table.getIsAllRowsSelected()}
24 onChange={table.getToggleAllRowsSelectedHandler()}
25 />
26 ),
27 cell: ({ row }) => (
28 <input
29 type="checkbox"
30 checked={row.getIsSelected()}
31 onChange={row.getToggleSelectedHandler()}
32 />
33 )
34 }
35
36 const table = useReactTable({
37 data,
38 columns: [selectionColumn, ...columns],
39 state: { rowSelection },
40 onRowSelectionChange: setRowSelection,
41 getCoreRowModel: getCoreRowModel()
42 })
43
44 const selectedRows = table.getSelectedRowModel().rows.map(r => r.original)
45
46 return (
47 <div>
48 <div className="mb-2 text-sm text-gray-500">
49 {selectedRows.length} of {data.length} selected
50 </div>
51
52 <table className="w-full">
53 {/* ... thead and tbody */}
54 </table>
55
56 {selectedRows.length > 0 && (
57 <div className="mt-4">
58 <button onClick={() => onSelectionChange?.(selectedRows)}>
59 Process Selected ({selectedRows.length})
60 </button>
61 </div>
62 )}
63 </div>
64 )
65}Best Practices#
- Use server-side pagination for large datasets - Don't load all data at once
- Memoize columns - Use
useMemoto prevent unnecessary re-renders - Add loading states - Show skeleton loaders while fetching data
- Handle empty states - Display meaningful messages when no data exists
- Make tables responsive - Use horizontal scroll or card layouts on mobile
Related Patterns#
- Data Tables - Advanced TanStack Table features
- Pagination - Server-side pagination
- URL State - Persist table state in URL