Claude-skills tanstack-table
TanStack Table v8 headless data tables with server-side features for Cloudflare Workers + D1. Use for pagination, filtering, sorting, virtualization, or encountering state management, TanStack Query coordination, URL sync errors.
git clone https://github.com/secondsky/claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/secondsky/claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/tanstack-table/skills/tanstack-table" ~/.claude/skills/secondsky-claude-skills-tanstack-table && rm -rf "$T"
plugins/tanstack-table/skills/tanstack-table/SKILL.mdTanStack 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
1. 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
2. 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
3. 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
4. Performance Optimization
- Virtualization with TanStack Virtual
- Large dataset rendering (10k+ rows)
- Memory-efficient patterns
- useVirtualizer() integration
5. 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
6. 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):
: v8.21.3 (stable)@tanstack/react-table
: v3.13.12@tanstack/react-virtual
: v8.21.3 (for fuzzy filtering)@tanstack/match-sorter-utils
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
1. Always Memoize Data and Columns
const data = useMemo(() => [...], [dependencies]) const columns = useMemo(() => [...], [])
2. Use Server-Side for Large Datasets
- Client-side: <1000 rows
- Server-side: 1000+ rows or frequently changing data
3. Coordinate Query Keys with Table State
queryKey: ['resource', pagination, filters, sorting]
4. Provide Loading States
if (isLoading) return <TableSkeleton /> if (error) return <ErrorMessage error={error} />
5. Use Column Helper for Type Safety
const columnHelper = createColumnHelper<YourType>() const columns = [ columnHelper.accessor('field', { /* fully typed */ }) ]
6. Virtualize Large Client-Side Tables
if (data.length > 1000) { // Use TanStack Virtual (see example above) }
7. Control Only the State You Need
- Keep
,sorting
,pagination
,filters
,visibility
,pinning
,order
in controlled state when you must persist or sync.selection - Avoid controlling
unless persisting drag state; it triggers frequent updates and can hurt performance.columnSizingInfo
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:
references/common-errors.md- 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:
references/server-side-patterns.md- 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:
references/query-integration.md- 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:
references/cloudflare-d1-examples.md- 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:
references/performance-virtualization.md- 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:
references/feature-controls.md- 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
- GitHub: https://github.com/TanStack/table
- 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