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-react

Basic 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#

  1. Use server-side pagination for large datasets - Don't load all data at once
  2. Memoize columns - Use useMemo to prevent unnecessary re-renders
  3. Add loading states - Show skeleton loaders while fetching data
  4. Handle empty states - Display meaningful messages when no data exists
  5. Make tables responsive - Use horizontal scroll or card layouts on mobile