Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    ataschz

    tanstack-table

    ataschz/tanstack-table
    Coding
    7

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Build headless data tables with TanStack Table v8. Server-side pagination, filtering, sorting, and virtualization for Cloudflare Workers + D1...

    SKILL.md

    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 */}
          <div className="overflow-x-auto">
            {table.getCenterHeaderGroups().map(/* render center headers */)}
            {table.getRowModel().rows.map(row => (
              <tr>{row.getCenterVisibleCells().map(/* render cells */)}</tr>
            ))}
          </div>
    
          {/* Right pinned columns */}
          <div className="sticky right-0 bg-background z-10">
            {table.getRightHeaderGroups().map(/* render right headers */)}
            {table.getRowModel().rows.map(row => (
              <tr>{row.getRightVisibleCells().map(/* render cells */)}</tr>
            ))}
          </div>
        </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.

    Recommended Servers
    Airtable
    Airtable
    ThinAir Data
    ThinAir Data
    Neon
    Neon
    Repository
    ataschz/tanstack-start-mastra-example