tanstack table

Headless data tables with server-side pagination, filtering, sorting, and virtualization for Cloudflare Workers + D1

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack table" with this command: npx skills add jezweb/claude-skills/jezweb-claude-skills-tanstack-table

TanStack Table

Headless data tables with server-side pagination, filtering, sorting, and virtualization for Cloudflare Workers + D1

Quick Start

Last Updated: 2026-01-09 Versions: @tanstack/react-table@8.21.3, @tanstack/react-virtual@3.13.18

npm install @tanstack/react-table@latest npm install @tanstack/react-virtual@latest # For virtualization

Basic Setup (CRITICAL: memoize data/columns to prevent infinite re-renders):

import { useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react'

const columns: ColumnDef<User>[] = [ { accessorKey: 'name', header: 'Name' }, { accessorKey: 'email', header: 'Email' }, ]

function UsersTable() { const data = useMemo(() => [...users], []) // Stable reference const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })

return ( <table> <thead> {table.getHeaderGroups().map(group => ( <tr key={group.id}> {group.headers.map(h => <th key={h.id}>{h.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

Cloudflare D1 API (pagination + filtering + sorting):

// Workers API: functions/api/users.ts export async function onRequestGet({ request, env }) { const url = new URL(request.url) const page = Number(url.searchParams.get('page')) || 0 const pageSize = 20 const search = url.searchParams.get('search') || '' const sortBy = url.searchParams.get('sortBy') || 'created_at' const sortOrder = url.searchParams.get('sortOrder') || 'DESC'

const { results } = await env.DB.prepare( SELECT * FROM users WHERE name LIKE ? OR email LIKE ? ORDER BY ${sortBy} ${sortOrder} LIMIT ? OFFSET ? ).bind(%${search}%, %${search}%, pageSize, page * pageSize).all()

const { total } = await env.DB.prepare('SELECT COUNT(*) as total FROM users').first()

return Response.json({ data: results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) }, }) }

Client-Side (TanStack Query + Table):

const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 }) const [columnFilters, setColumnFilters] = useState([]) const [sorting, setSorting] = useState([])

// CRITICAL: Include ALL state in query key const { data, isLoading } = useQuery({ queryKey: ['users', pagination, columnFilters, sorting], queryFn: async () => { const params = new URLSearchParams({ page: pagination.pageIndex, search: columnFilters.find(f => f.id === 'search')?.value || '', sortBy: sorting[0]?.id || 'created_at', sortOrder: sorting[0]?.desc ? 'DESC' : 'ASC', }) return fetch(/api/users?${params}).then(r => r.json()) }, })

const table = useReactTable({ data: data?.data ?? [], columns, getCoreRowModel: getCoreRowModel(), // CRITICAL: manual* flags tell table server handles these manualPagination: true, manualFiltering: true, manualSorting: true, pageCount: data?.pagination.pageCount ?? 0, state: { pagination, columnFilters, sorting }, onPaginationChange: setPagination, onColumnFiltersChange: setColumnFilters, onSortingChange: setSorting, })

Virtualization (1000+ Rows)

Render only visible rows for performance:

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualizedTable() { const containerRef = useRef<HTMLDivElement>(null) const table = useReactTable({ data: largeDataset, columns, getCoreRowModel: getCoreRowModel() }) const { rows } = table.getRowModel()

const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => containerRef.current, estimateSize: () => 50, // Row height px overscan: 10, })

return ( <div ref={containerRef} style={{ height: '600px', overflow: 'auto' }}> <table style={{ height: ${rowVirtualizer.getTotalSize()}px }}> <tbody> {rowVirtualizer.getVirtualItems().map(virtualRow => { const row = rows[virtualRow.index] return ( <tr key={row.id} style={{ position: 'absolute', transform: translateY(${virtualRow.start}px) }}> {row.getVisibleCells().map(cell => <td key={cell.id}>{cell.renderValue()}</td>)} </tr> ) })} </tbody> </table> </div> ) }

Warning: Hidden Containers (Tabs/Modals)

Known Issue: When using virtualization inside tabbed content or modals that hide inactive content with display: none , the virtualizer continues performing layout calculations while hidden, causing:

  • Infinite re-render loops (large datasets: 50k+ rows)

  • Incorrect scroll position when tab becomes visible

  • Empty table or reset scroll (small datasets)

Source: GitHub Issue #6109

Prevention:

const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => containerRef.current, estimateSize: () => 50, overscan: 10, // Disable when container is hidden to prevent infinite re-renders enabled: containerRef.current?.getClientRects().length !== 0, })

// OR: Conditionally render instead of hiding with CSS {isVisible && <VirtualizedTable />}

Column/Row Pinning

Pin columns or rows to keep them visible during horizontal/vertical scroll:

import { useReactTable, getCoreRowModel } from '@tanstack/react-table'

const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), // Enable pinning enableColumnPinning: true, enableRowPinning: true, // Initial pinning state initialState: { columnPinning: { left: ['select', 'name'], // Pin to left right: ['actions'], // Pin to right }, }, })

// Render with pinned columns function PinnedTable() { return ( <div className="flex"> {/* Left pinned columns /} <div className="sticky left-0 bg-background z-10"> {table.getLeftHeaderGroups().map(/ render left headers /)} {table.getRowModel().rows.map(row => ( <tr>{row.getLeftVisibleCells().map(/ render cells */)}</tr> ))} </div>

  {/* Center scrollable columns */}
  &#x3C;div className="overflow-x-auto">
    {table.getCenterHeaderGroups().map(/* render center headers */)}
    {table.getRowModel().rows.map(row => (
      &#x3C;tr>{row.getCenterVisibleCells().map(/* render cells */)}&#x3C;/tr>
    ))}
  &#x3C;/div>

  {/* Right pinned columns */}
  &#x3C;div className="sticky right-0 bg-background z-10">
    {table.getRightHeaderGroups().map(/* render right headers */)}
    {table.getRowModel().rows.map(row => (
      &#x3C;tr>{row.getRightVisibleCells().map(/* render cells */)}&#x3C;/tr>
    ))}
  &#x3C;/div>
&#x3C;/div>

) }

// Toggle pinning programmatically column.pin('left') // Pin column to left column.pin('right') // Pin column to right column.pin(false) // Unpin column row.pin('top') // Pin row to top row.pin('bottom') // Pin row to bottom

Warning: Column Pinning with Column Groups

Known Issue: Pinning parent group columns (created with columnHelper.group() ) causes incorrect positioning and duplicated headers. column.getStart('left') returns wrong values for group headers.

Source: GitHub Issue #5397

Prevention:

// Disable pinning for grouped columns const isPinnable = (column) => !column.parent

// OR: Pin individual columns within group, not the group itself table.getColumn('firstName')?.pin('left') table.getColumn('lastName')?.pin('left') // Don't pin the parent group column

Row Expanding (Nested Data)

Show/hide child rows or additional details:

import { useReactTable, getCoreRowModel, getExpandedRowModel } from '@tanstack/react-table'

// Data with nested children const data = [ { id: 1, name: 'Parent Row', subRows: [ { id: 2, name: 'Child Row 1' }, { id: 3, name: 'Child Row 2' }, ], }, ]

const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), // Required for expanding getSubRows: row => row.subRows, // Tell table where children are })

// Render with expand button function ExpandableTable() { return ( <tbody> {table.getRowModel().rows.map(row => ( <> <tr key={row.id}> <td> {row.getCanExpand() && ( <button onClick={row.getToggleExpandedHandler()}> {row.getIsExpanded() ? '▼' : '▶'} </button> )} </td> {row.getVisibleCells().map(cell => ( <td key={cell.id} style={{ paddingLeft: ${row.depth * 20}px }}> {cell.renderValue()} </td> ))} </tr> </> ))} </tbody> ) }

// Control expansion programmatically table.toggleAllRowsExpanded() // Expand/collapse all row.toggleExpanded() // Toggle single row table.getIsAllRowsExpanded() // Check if all expanded

Detail Rows (custom content, not nested data):

function DetailRow({ row }) { if (!row.getIsExpanded()) return null

return ( <tr> <td colSpan={columns.length}> <div className="p-4 bg-muted"> Custom detail content for row {row.id} </div> </td> </tr> ) }

Row Grouping

Group rows by column values:

import { useReactTable, getCoreRowModel, getGroupedRowModel } from '@tanstack/react-table'

const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getGroupedRowModel: getGroupedRowModel(), // Required for grouping getExpandedRowModel: getExpandedRowModel(), // Groups are expandable initialState: { grouping: ['status'], // Group by 'status' column }, })

// Column with aggregation const columns = [ { accessorKey: 'status', header: 'Status', }, { accessorKey: 'amount', header: 'Amount', aggregationFn: 'sum', // Sum grouped values aggregatedCell: ({ getValue }) => Total: ${getValue()}, }, ]

// Render grouped table function GroupedTable() { return ( <tbody> {table.getRowModel().rows.map(row => ( <tr key={row.id}> {row.getVisibleCells().map(cell => ( <td key={cell.id}> {cell.getIsGrouped() ? ( // Grouped cell - show group header with expand toggle <button onClick={row.getToggleExpandedHandler()}> {row.getIsExpanded() ? '▼' : '▶'} {cell.renderValue()} ({row.subRows.length}) </button> ) : cell.getIsAggregated() ? ( // Aggregated cell - show aggregation result cell.renderValue() ) : cell.getIsPlaceholder() ? null : ( // Regular cell cell.renderValue() )} </td> ))} </tr> ))} </tbody> ) }

// Built-in aggregation functions // 'sum', 'min', 'max', 'extent', 'mean', 'median', 'unique', 'uniqueCount', 'count'

Warning: Performance Bottleneck with Grouping (Community-sourced)

Known Issue: The grouping feature causes significant performance degradation on medium-to-large datasets. With grouping enabled, render times can increase from <1 second to 30-40 seconds on 50k rows due to excessive memory usage in createRow calculations.

Source: Blog Post (JP Camara) | GitHub Issue #5926

Verified: Community testing + GitHub issue report

Prevention:

// 1. Use server-side grouping for large datasets // 2. Implement pagination to limit rows per page // 3. Disable grouping for 10k+ rows const shouldEnableGrouping = data.length < 10000

// 4. OR: Use React.memo on row components const MemoizedRow = React.memo(TableRow)

Known Issues & Solutions

Issue #1: Infinite Re-Renders

  • Error: Table re-renders infinitely, browser freezes

  • Cause: data or columns references change on every render

  • Fix: Use useMemo(() => [...], []) or define data/columns outside component

Issue #2: Query + Table State Mismatch

  • Error: Query refetches but pagination state not synced, stale data

  • Cause: Query key missing table state (pagination, filters, sorting)

  • Fix: Include ALL state in query key: queryKey: ['users', pagination, columnFilters, sorting]

Issue #3: Server-Side Features Not Working

  • Error: Pagination/filtering/sorting doesn't trigger API calls

  • Cause: Missing manual* flags

  • Fix: Set manualPagination: true , manualFiltering: true , manualSorting: true

  • provide pageCount

Issue #4: TypeScript "Cannot Find Module"

  • Error: Import errors for createColumnHelper

  • Fix: Import from @tanstack/react-table (NOT @tanstack/table-core )

Issue #5: Sorting Not Working Server-Side

  • Error: Clicking sort headers doesn't update data

  • Cause: Sorting state not in query key/API params

  • Fix: Include sorting in query key, add sort params to API call, set manualSorting: true

  • onSortingChange

Issue #6: Poor Performance (1000+ Rows)

  • Error: Table slow/laggy with large datasets

  • Fix: Use TanStack Virtual for client-side OR implement server-side pagination

Issue #7: React Compiler Incompatibility (React 19+)

  • Error: "Table doesn't re-render when data changes" (with React Compiler enabled)

  • Source: GitHub Issue #5567

  • Why It Happens: React Compiler's automatic memoization conflicts with table core instance, preventing re-renders when data/state changes

  • Prevention: Add "use no memo" directive at top of components using useReactTable :

"use no memo"

function TableComponent() { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) // Now works correctly with React Compiler }

Note: This issue also affects column visibility and row selection. Full fix coming in v9.

Issue #8: Server-Side Pagination Row Selection Bug

  • Error: toggleAllRowsSelected(false) only deselects current page, not all pages

  • Source: GitHub Issue #5929

  • Why It Happens: Selection state persists across pages (intentional for server-side use cases), but header checkbox state is calculated incorrectly

  • Prevention: Manually clear selection state when toggling off:

const toggleAllRows = (value: boolean) => { if (!value) { table.setRowSelection({}) // Clear entire selection object } else { table.toggleAllRowsSelected(true) } }

Issue #9: Client-Side onPaginationChange Returns Incorrect pageIndex

  • Error: onPaginationChange always returns pageIndex: 0 instead of current page

  • Source: GitHub Issue #5970

  • Why It Happens: Client-side pagination mode has state tracking bug (only occurs in client mode, works correctly in server/manual mode)

  • Prevention: Switch to manual pagination for correct behavior:

// Instead of relying on client-side pagination const table = useReactTable({ data, columns, manualPagination: true, // Forces correct state tracking pageCount: Math.ceil(data.length / pagination.pageSize), state: { pagination }, onPaginationChange: setPagination, })

Issue #10: Row Selection Not Cleaned Up When Data Removed

  • Error: Selected rows that no longer exist in data remain in selection state

  • Source: GitHub Issue #5850

  • Why It Happens: Intentional behavior to support server-side pagination (where rows disappear from current page but should stay selected)

  • Prevention: Manually clean up selection when removing data:

const removeRow = (idToRemove: string) => { // Remove from data setData(data.filter(row => row.id !== idToRemove))

// Clean up selection if it was selected const { rowSelection } = table.getState() if (rowSelection[idToRemove]) { table.setRowSelection((old) => { const filtered = Object.entries(old).filter(([id]) => id !== idToRemove) return Object.fromEntries(filtered) }) } }

// OR: Use table.resetRowSelection(true) to clear all

Issue #11: Performance Degradation with React DevTools Open

  • Error: Table performance significantly degrades with React DevTools open (development only)

  • Why It Happens: DevTools inspects table instance and row models on every render, especially noticeable with 500+ rows

  • Fix: Close React DevTools during performance testing. This is not a production issue.

Issue #12: TypeScript getValue() Type Inference with Grouped Columns

  • Error: getValue() returns unknown instead of accessor's actual type inside columnHelper.group()

  • Source: GitHub Issue #5860

  • Fix: Manually specify type or use renderValue() :

// Option 1: Type assertion cell: (info) => { const value = info.getValue() as string return value.toUpperCase() }

// Option 2: Use renderValue() (better type inference) cell: (info) => { const value = info.renderValue() return typeof value === 'string' ? value.toUpperCase() : value }

Related Skills: tanstack-query (data fetching), cloudflare-d1 (database backend), tailwind-v4-shadcn (UI styling)

Last verified: 2026-01-21 | Skill version: 2.0.0 | Changes: Added 7 new known issues from TIER 1-2 research findings (React 19 Compiler, server-side row selection, virtualization in hidden containers, client-side pagination bug, column pinning with groups, row selection cleanup, DevTools performance, TypeScript getValue). Error count: 6 → 12.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tailwind-v4-shadcn

No summary provided by upstream source.

Repository SourceNeeds Review
-2.7K
jezweb
General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
-2.5K
jezweb
General

fastapi

No summary provided by upstream source.

Repository SourceNeeds Review