安装
npx skills add https://github.com/jezweb/claude-skills --skill 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 /
}
<
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
}
← 返回排行榜