
tanstack-table
Build headless data tables with TanStack Table v8. Server-side pagination, filtering, sorting, and virtualization for Cloudflare Workers + D1. Prevents 12 documented errors. Use when building tables with large datasets, coordinating with TanStack Query, or fixing state management, performance, or React 19+ compatibility issues.
- Installs
- 745
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/jezweb/claude-skills --skill tanstack-table- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads tanstack-table automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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:
dataorcolumnsreferences 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+ providepageCount
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
sortingin query key, add sort params to API call, setmanualSorting: 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 usinguseReactTable:
"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:
onPaginationChangealways returnspageIndex: 0instead 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()returnsunknowninstead of accessor's actual type insidecolumnHelper.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.
FAQ
What does the tanstack-table skill do?
Build headless data tables with TanStack Table v8. Server-side pagination, filtering, sorting, and virtualization for Cloudflare Workers + D1. Prevents 12 documented errors. Use when building tables with large datasets, coordinating with TanStack Query, or fixing state management, performance, or React 19+ compatibility issues.
How do I install tanstack-table?
Run npx skills add https://github.com/jezweb/claude-skills --skill tanstack-table in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/tanstack-table/ (personal) or .claude/skills/tanstack-table/ (project).
Which AI agents can use tanstack-table?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is tanstack-table safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains tanstack-table?
It is published by jezweb in jezweb/claude-skills. It has 745 recorded installs.
More from jezweb/claude-skills
shadcn-ui
jezweb/claude-skills
Install and configure shadcn/ui components for React projects. Guides component selection, installation order, dependency management, customisation with semantic tokens, and common UI recipes (forms, data tables, navigation, modals). Use after tailwind-theme-builder has set up the theme infrastructure, when adding components, building forms, creating data tables, or setting up navigation.
color-palette
jezweb/claude-skills
Generate complete, accessible colour palettes from a single brand hex. Produces 11-shade scale (50-950), semantic tokens, dark mode variants, Tailwind v4 CSS output, WCAG contrast checks. Use whenever the user supplies a brand hex and asks for a palette, mentions setting up a design system, wants Tailwind theme colours from a brand colour, or asks to check colour accessibility / contrast.
tailwind-theme-builder
jezweb/claude-skills
Set up Tailwind v4 + shadcn/ui themed UI with dark mode. Install deps, configure CSS variables via @theme inline, wire dark mode toggle, verify. Use whenever the user mentions Tailwind v4, setting up Tailwind theming, shadcn/ui colours, dark mode, or troubleshooting colours not working, tw-animate-css errors, @theme inline conflicts, @apply breaking after upgrade, or v3 → v4 migration issues.
wordpress-elementor
jezweb/claude-skills
Edit Elementor pages and manage templates on WordPress sites via browser automation (for visual / structural changes) or WP-CLI (for safe text replacements). Use whenever the user wants to edit an Elementor page, update text in Elementor widgets, apply or manage Elementor templates, or make content changes to pages built with the Elementor page builder.
ux-audit
jezweb/claude-skills
Walk through a live web app AS a real user to find usability + behavioural bugs that static reviews miss. REQUIRES proof of interaction (typing, clicking, sending, observing) before any verdict — a sweep that didn't interact terminates with verdict 'Incomplete'. Walks threads, exercises every element, runs the multi-pane stress matrix, visual polish sweep, component perfection checklist, automated a11y (axe-core), pragmatic performance budget (LCP/CLS/INP), scenario battery (11 scenarios), and stress recipes including the real-flavour data battery. Hard gates: console errors/warnings = 0, network 5xx = 0, layout collapse = 0, axe Critical/Serious = 0, perf budget green. Audit-the-audit meta-check rejects rushed reports. Each finding has reproduction steps, evidence path, and suspected code location. Trigger with 'ux audit', 'walkthrough', 'qa sweep', 'audit the app', 'dogfood this', 'check all pages', 'find what's broken', 'stress the UI'.
tailwind-v4-shadcn
jezweb/claude-skills
Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step pattern: CSS variables, Tailwind mapping, base styles, automatic dark mode. Prevents 8 documented errors. Use when initializing React projects with Tailwind v4, or fixing colors not working, tw-animate-css errors, @theme inline dark mode conflicts, @apply breaking, v3 migration issues.
Popular frontend & design skills
setup-matt-pocock-skills
mattpocock/skills
Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
frontend-design
anthropics/skills
Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
hyperframes-animation
heygen-com/hyperframes
All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.
prototype
mattpocock/skills
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.
vercel-react-best-practices
vercel-labs/agent-skills
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.