TanStack Table Skill
Build production-ready, headless data tables with TanStack Table v8, optimized for server-side patterns and Cloudflare Workers integration.
When to Use This Skill
Auto-triggers when you mention:
-
"data table" or "datagrid"
-
"server-side pagination" or "server-side filtering"
-
"TanStack Table" or "React Table"
-
"table with large dataset"
-
"paginate/filter/sort with API"
-
"Cloudflare D1 table integration"
-
"virtualize table" or "large list performance"
Use this skill when:
-
Building data tables with pagination, filtering, or sorting
-
Implementing server-side table features (API-driven)
-
Integrating tables with TanStack Query for data fetching
-
Working with large datasets (1000+ rows) needing virtualization
-
Connecting tables to Cloudflare D1 databases
-
Need headless table logic without opinionated UI
-
Migrating from other table libraries to TanStack Table v8
What This Skill Provides
- Production Templates (7)
-
Basic client-side table - Simple table with local data
-
Server-paginated table - API-driven pagination with TanStack Query
-
D1 database integration - Cloudflare D1 + Workers API + Table
-
Column configuration patterns - Type-safe column definitions
-
Controlled table state - Column visibility, pinning, ordering, fuzzy/global filtering, row selection
-
Virtualized large dataset - Performance optimization with TanStack Virtual
-
shadcn/ui styled table - Integration with Tailwind v4 + shadcn
- Server-Side Patterns
-
Pagination with API backends
-
Filtering with query parameters
-
Sorting with database queries
-
State management (page, filters, sorting)
-
URL synchronization
-
TanStack Query coordination
- Cloudflare Integration
-
D1 database query patterns
-
Workers API endpoints for table data
-
Pagination + filtering + sorting in SQL
-
Bindings setup (wrangler.jsonc)
-
Client-side integration patterns
- Performance Optimization
-
Virtualization with TanStack Virtual
-
Large dataset rendering (10k+ rows)
-
Memory-efficient patterns
-
useVirtualizer() integration
- Feature Controls & UX
-
Column visibility toggles and pinning (frozen columns)
-
Column ordering and sizing defaults
-
Global + fuzzy search and faceted filters
-
Row selection and row pinning patterns
-
Controlled state checklist to avoid perf regressions
- Error Prevention
Documents and prevents 6+ common issues:
-
Server-side state management confusion
-
TanStack Query integration errors (query key coordination)
-
Column filtering with API backends
-
Manual sorting setup mistakes
-
URL state synchronization issues
-
Large dataset performance problems
-
Over-controlling table state (columnSizingInfo) causing extra renders
Quick Start
Installation
Core table library
bun add @tanstack/react-table@latest
Optional: For virtualization (1000+ rows)
bun add @tanstack/react-virtual@latest
Optional: For fuzzy/global search
bun add @tanstack/match-sorter-utils@latest
Latest verified versions (as of 2025-12-09):
-
@tanstack/react-table : v8.21.3 (stable)
-
@tanstack/react-virtual : v3.13.12
-
@tanstack/match-sorter-utils : v8.21.3 (for fuzzy filtering)
React support: Works on React 16.8+ through React 19; React Compiler is not supported.
Basic Client-Side Table
import { useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react'
interface User { id: string name: string email: string }
const columns: ColumnDef<User>[] = [ { accessorKey: 'id', header: 'ID' }, { accessorKey: 'name', header: 'Name' }, { accessorKey: 'email', header: 'Email' }, ]
function UsersTable() { // CRITICAL: Memoize data and columns to prevent infinite re-renders const data = useMemo<User[]>(() => [ { id: '1', name: 'Alice', email: 'alice@example.com' }, { id: '2', name: 'Bob', email: 'bob@example.com' }, ], [])
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), // Required })
return ( <table> <thead> {table.getHeaderGroups().map(headerGroup => ( <tr key={headerGroup.id}> {headerGroup.headers.map(header => ( <th key={header.id}> {header.isPlaceholder ? null : header.column.columnDef.header} </th> ))} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map(row => ( <tr key={row.id}> {row.getVisibleCells().map(cell => ( <td key={cell.id}> {cell.renderValue()} </td> ))} </tr> ))} </tbody> </table> ) }
Server-Side Patterns (Recommended for Large Datasets)
Pattern 1: Server-Side Pagination with TanStack Query
Cloudflare Workers API Endpoint:
// src/routes/api/users.ts import { Env } from '../../types'
export async function onRequestGet(context: { request: Request; env: Env }) { const url = new URL(context.request.url) const page = Number(url.searchParams.get('page')) || 0 const pageSize = Number(url.searchParams.get('pageSize')) || 20
const offset = page * pageSize
// Query D1 database
const { results, meta } = await context.env.DB.prepare( SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT ? OFFSET ? ).bind(pageSize, offset).all()
// Get total count for pagination
const countResult = await context.env.DB.prepare( SELECT COUNT(*) as total FROM users ).first<{ total: number }>()
return Response.json({ data: results, pagination: { page, pageSize, total: countResult?.total || 0, pageCount: Math.ceil((countResult?.total || 0) / pageSize), }, }) }
Client-Side Table with TanStack Query:
import { useReactTable, getCoreRowModel, PaginationState } from '@tanstack/react-table' import { useQuery } from '@tanstack/react-query' import { useState } from 'react'
function ServerPaginatedTable() { const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 20, })
// TanStack Query fetches data
const { data, isLoading } = useQuery({
queryKey: ['users', pagination.pageIndex, pagination.pageSize],
queryFn: async () => {
const response = await fetch(
/api/users?page=${pagination.pageIndex}&pageSize=${pagination.pageSize}
)
return response.json()
},
})
// TanStack Table manages display const table = useReactTable({ data: data?.data ?? [], columns, getCoreRowModel: getCoreRowModel(), // Server-side pagination config manualPagination: true, // CRITICAL: Tell table pagination is manual pageCount: data?.pagination.pageCount ?? 0, state: { pagination }, onPaginationChange: setPagination, })
if (isLoading) return <div>Loading...</div>
return ( <div> <table>{/* render table */}</table>
{/* Pagination controls */}
<div>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</button>
<span>
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</span>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</button>
</div>
</div>
) }
Pattern 2: Server-Side Filtering
API with Filter Support:
export async function onRequestGet(context: { request: Request; env: Env }) { const url = new URL(context.request.url) const search = url.searchParams.get('search') || ''
const { results } = await context.env.DB.prepare( SELECT * FROM users WHERE name LIKE ? OR email LIKE ? LIMIT 20 ).bind(%${search}%, %${search}%).all()
return Response.json({ data: results }) }
Client-Side:
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const { data } = useQuery({
queryKey: ['users', columnFilters],
queryFn: async () => {
const search = columnFilters.find(f => f.id === 'search')?.value || ''
return fetch(/api/users?search=${search}).then(r => r.json())
},
})
const table = useReactTable({ data: data?.data ?? [], columns, getCoreRowModel: getCoreRowModel(), manualFiltering: true, // CRITICAL: Server handles filtering state: { columnFilters }, onColumnFiltersChange: setColumnFilters, })
Virtualization for Large Datasets
For 1000+ rows, use TanStack Virtual to only render visible rows:
import { useVirtualizer } from '@tanstack/react-virtual' import { useRef } from 'react'
function VirtualizedTable() { const tableContainerRef = useRef<HTMLDivElement>(null)
const table = useReactTable({ data: largeDataset, // 10k+ rows columns, getCoreRowModel: getCoreRowModel(), })
const { rows } = table.getRowModel()
// Virtualize rows const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => tableContainerRef.current, estimateSize: () => 50, // Row height in px overscan: 10, // Render 10 extra rows for smooth scrolling })
return (
<div ref={tableContainerRef} style={{ height: '600px', overflow: 'auto' }}>
<table style={{ height: ${rowVirtualizer.getTotalSize()}px }}>
<thead>{/* header */}</thead>
<tbody>
{rowVirtualizer.getVirtualItems().map(virtualRow => {
const row = rows[virtualRow.index]
return (
<tr
key={row.id}
style={{
position: 'absolute',
transform: translateY(${virtualRow.start}px),
width: '100%',
}}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>{cell.renderValue()}</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
Common Errors & Solutions
Error 1: Infinite Re-Renders
Problem: Table re-renders infinitely, browser freezes.
Cause: data or columns references change on every render.
Solution: Always use useMemo or useState :
// ❌ BAD: New array reference every render function Table() { const data = [{ id: 1 }] // Creates new array! const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) }
// ✅ GOOD: Stable reference function Table() { const data = useMemo(() => [{ id: 1 }], []) // Stable const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) }
// ✅ ALSO GOOD: Define outside component const data = [{ id: 1 }] function Table() { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) }
Error 2: TanStack Query + Table State Mismatch
Problem: Query refetches but pagination state not in sync, causing stale data.
Solution: Include ALL table state in query key:
// ❌ BAD: Missing pagination in query key
const { data } = useQuery({
queryKey: ['users'], // Doesn't include page!
queryFn: () => fetch(/api/users?page=${pagination.pageIndex}).then(r => r.json())
})
// ✅ GOOD: Complete query key
const { data } = useQuery({
queryKey: ['users', pagination.pageIndex, pagination.pageSize, columnFilters, sorting],
queryFn: () => {
const params = new URLSearchParams({
page: pagination.pageIndex.toString(),
pageSize: pagination.pageSize.toString(),
// ... filters, sorting
})
return fetch(/api/users?${params}).then(r => r.json())
}
})
Error 3: Server-Side Features Not Working
Problem: Pagination/filtering/sorting doesn't trigger API calls.
Solution: Set manual* flags to true :
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), // CRITICAL: Tell table these are server-side manualPagination: true, manualFiltering: true, manualSorting: true, pageCount: serverPageCount, // Must provide total page count })
Error 4: TypeScript "Cannot Find Module" for Column Helper
Problem: Import errors for createColumnHelper .
Solution: Import from correct path:
// ❌ BAD: Wrong path import { createColumnHelper } from '@tanstack/table-core'
// ✅ GOOD: Correct path import { createColumnHelper } from '@tanstack/react-table'
// Usage for type-safe columns const columnHelper = createColumnHelper<User>() const columns = [ columnHelper.accessor('name', { header: 'Name', cell: info => info.getValue(), // Fully typed! }), ]
Error 5: Sorting Not Working with Server-Side
Problem: Clicking sort headers doesn't update data.
Solution: Include sorting in query key and API call:
const [sorting, setSorting] = useState<SortingState>([])
const { data } = useQuery({
queryKey: ['users', pagination, sorting], // Include sorting
queryFn: async () => {
const sortParam = sorting[0]
? &sortBy=${sorting[0].id}&sortOrder=${sorting[0].desc ? 'desc' : 'asc'}
: ''
return fetch(/api/users?page=${pagination.pageIndex}${sortParam}).then(r => r.json())
}
})
const table = useReactTable({ data: data?.data ?? [], columns, getCoreRowModel: getCoreRowModel(), manualSorting: true, state: { sorting }, onSortingChange: setSorting, })
Error 6: Poor Performance with Large Datasets
Problem: Table slow/laggy with 1000+ rows.
Solution: Use virtualization (see example above) or implement server-side pagination.
Integration with Existing Skills
With tanstack-query Skill
TanStack Table + TanStack Query is the recommended pattern:
// Query handles data fetching + caching const { data, isLoading } = useQuery({ queryKey: ['users', tableState], queryFn: fetchUsers, })
// Table handles display + interactions const table = useReactTable({ data: data?.data ?? [], columns, getCoreRowModel: getCoreRowModel(), })
With cloudflare-d1 Skill
// Cloudflare Workers API (from cloudflare-d1 skill patterns) export async function onRequestGet({ env }: { env: Env }) { const { results } = await env.DB.prepare('SELECT * FROM users LIMIT 20').all() return Response.json({ data: results }) }
// Client-side table consumes D1 data const { data } = useQuery({ queryKey: ['users'], queryFn: () => fetch('/api/users').then(r => r.json()) })
With tailwind-v4-shadcn Skill
Use shadcn/ui Table components with TanStack Table logic:
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'
function StyledTable() { const table = useReactTable({ /* config */ })
return ( <Table> <TableHeader> {table.getHeaderGroups().map(headerGroup => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map(header => ( <TableHead key={header.id}> {header.column.columnDef.header} </TableHead> ))} </TableRow> ))} </TableHeader> <TableBody> {table.getRowModel().rows.map(row => ( <TableRow key={row.id}> {row.getVisibleCells().map(cell => ( <TableCell key={cell.id}> {cell.renderValue()} </TableCell> ))} </TableRow> ))} </TableBody> </Table> ) }
Best Practices
- Always Memoize Data and Columns
const data = useMemo(() => [...], [dependencies]) const columns = useMemo(() => [...], [])
- Use Server-Side for Large Datasets
-
Client-side: <1000 rows
-
Server-side: 1000+ rows or frequently changing data
- Coordinate Query Keys with Table State
queryKey: ['resource', pagination, filters, sorting]
- Provide Loading States
if (isLoading) return <TableSkeleton /> if (error) return <ErrorMessage error={error} />
- Use Column Helper for Type Safety
const columnHelper = createColumnHelper<YourType>() const columns = [ columnHelper.accessor('field', { /* fully typed */ }) ]
- Virtualize Large Client-Side Tables
if (data.length > 1000) { // Use TanStack Virtual (see example above) }
- Control Only the State You Need
-
Keep sorting , pagination , filters , visibility , pinning , order , selection in controlled state when you must persist or sync.
-
Avoid controlling columnSizingInfo unless persisting drag state; it triggers frequent updates and can hurt performance.
Templates Reference
All templates available in ~/.claude/skills/tanstack-table/templates/ :
-
package.json - Dependencies and versions
-
basic-client-table.tsx - Simple client-side table
-
server-paginated-table.tsx - Server-side pagination with Query
-
d1-database-example.tsx - Cloudflare D1 integration
-
column-configuration.tsx - Type-safe column patterns
-
controlled-table-state.tsx - Visibility, pinning, ordering, fuzzy/global filtering, selection
-
virtualized-large-dataset.tsx - Performance with Virtual
-
shadcn-styled-table.tsx - Tailwind v4 + shadcn UI styling
Reference Docs
Deep-dive guides in ~/.claude/skills/tanstack-table/references/ :
-
server-side-patterns.md - Pagination, filtering, sorting with APIs
-
query-integration.md - Coordinating with TanStack Query
-
cloudflare-d1-examples.md - Workers + D1 complete examples
-
performance-virtualization.md - TanStack Virtual guide
-
common-errors.md - All 6+ documented issues with solutions
-
feature-controls.md - Controlled state, visibility, pinning, ordering, fuzzy/global filtering, selection
When to Load References
Claude should suggest loading these reference files based on user needs:
Load references/common-errors.md when:
-
User encounters infinite re-renders or table freezing
-
Query data not syncing with pagination state changes
-
Server-side features (pagination/filtering/sorting) not triggering API calls
-
TypeScript errors with column helper imports
-
Sorting state changes not updating API calls
-
Performance problems with 1000+ rows client-side
-
Any error message mentioned in the 6 documented issues
Load references/server-side-patterns.md when:
-
User asks about implementing pagination with API backends
-
Need to build filtering with backend query parameters
-
Implementing sorting tied to database queries
-
Building Cloudflare Workers or any API endpoints for table data
-
Coordinating table state (page, filters, sort) with server calls
-
Questions about manualPagination, manualFiltering, or manualSorting flags
Load references/query-integration.md when:
-
Coordinating TanStack Table + TanStack Query together
-
Query keys and table state synchronization issues
-
Refetch patterns when pagination/filter/sort changes
-
Query key composition with table state
-
Stale data issues with server-side tables
Load references/cloudflare-d1-examples.md when:
-
Building Cloudflare Workers API endpoints for table data
-
Writing D1 database queries with pagination/filtering
-
Need complete end-to-end Cloudflare integration examples
-
SQL query patterns for table features (LIMIT/OFFSET, WHERE, ORDER BY)
-
wrangler.jsonc bindings setup for D1 + table
Load references/performance-virtualization.md when:
-
Working with 1000+ row datasets client-side
-
TanStack Virtual integration questions
-
Memory-efficient rendering patterns
-
useVirtualizer() hook usage
-
Large table performance optimization
-
Questions about row virtualization or scroll performance
Load references/feature-controls.md when:
-
Need column visibility, pinning, or ordering controls
-
Building toolbars (global search, toggles) or syncing state to URL/localStorage
-
Implementing fuzzy/global search or faceted filters
-
Setting up row selection/pinning or controlled pagination/sorting
Token Efficiency
Without this skill:
-
~8,000 tokens: Research v8 changes, server-side patterns, Query integration
-
3-4 common errors encountered
-
30-45 minutes total time
With this skill:
-
~3,500 tokens: Direct templates, error prevention
-
0 errors (all documented issues prevented)
-
10-15 minutes total time
Savings: ~55-65% tokens, ~70% time
Production Validation
Tested with:
-
React 19.2
-
Vite 6.0
-
TypeScript 5.8
-
Cloudflare Workers (Wrangler 4.0)
-
TanStack Query v5.90.7 (tanstack-query skill)
-
Tailwind v4 + shadcn/ui (tailwind-v4-shadcn skill)
Stack compatibility:
-
✅ Cloudflare Workers + Static Assets
-
✅ Cloudflare D1 database
-
✅ TanStack Query integration
-
✅ React 19.2+ server components
-
✅ TypeScript strict mode
-
✅ Vite 6.0+ build optimization
Further Reading
-
Official Docs: https://tanstack.com/table/latest
-
TanStack Virtual: https://tanstack.com/virtual/latest
-
Cloudflare D1 Skill: ~/.claude/skills/cloudflare-d1/
-
TanStack Query Skill: ~/.claude/skills/tanstack-query/
Last Updated: 2025-12-09 Skill Version: 1.1.0 Library Version: @tanstack/react-table v8.21.3