react-table-virtualized
v0.1.1
Published
Seamless virtualization for TanStack Table using TanStack Virtual
Maintainers
Readme
react-table-virtualized
Seamless virtualization for TanStack Table using TanStack Virtual
Features
- 🎯 Single Hook API - One hook to rule them all
- 🪶 Lightweight - < 1KB gzipped
- ⚡ Zero Dependencies - Only peer dependencies
- 💪 TypeScript - Full type safety out of the box
- 🔥 Fully Compatible - Works with all TanStack Table features (sorting, filtering, grouping, etc.)
- 📦 Tree-shakeable - ESM and CJS builds
Why?
TanStack Table is amazing for managing table state, but it doesn't handle virtualization. TanStack Virtual is perfect for virtualizing lists, but it doesn't know about tables. This package seamlessly wires them together with a simple, intuitive API.
The result? Render 50,000+ rows smoothly while only keeping ~20 DOM nodes in memory.
Installation
npm install react-table-virtualized @tanstack/react-table @tanstack/react-virtualyarn add react-table-virtualized @tanstack/react-table @tanstack/react-virtualpnpm add react-table-virtualized @tanstack/react-table @tanstack/react-virtualQuick Start
import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table'
import { useVirtualTable } from 'react-table-virtualized'
function MyTable({ data, columns }) {
// 1. Set up react-table as usual
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
// 2. Add virtualization with one hook
const { rows, virtualizer, containerRef } = useVirtualTable({
table,
estimateSize: 35, // estimated row height in pixels
overscan: 5, // number of rows to render outside visible area
})
// 3. Render
return (
<div ref={containerRef} style={{ height: '500px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{/* Header */}
<div style={{ display: 'flex', position: 'sticky', top: 0 }}>
{table.getHeaderGroups().map((headerGroup) =>
headerGroup.headers.map((header) => (
<div key={header.id} style={{ width: header.getSize() }}>
{flexRender(header.column.columnDef.header, header.getContext())}
</div>
))
)}
</div>
{/* Virtual Rows */}
{rows.map((virtualRow) => {
const row = table.getRowModel().rows[virtualRow.index]
return (
<div
key={row.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{row.getVisibleCells().map((cell) => (
<div key={cell.id} style={{ width: cell.column.getSize() }}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
))}
</div>
)
})}
</div>
</div>
)
}That's it! You now have a fully virtualized table that works with all TanStack Table features.
API Reference
useVirtualTable(options)
The main hook that connects TanStack Table with TanStack Virtual.
Parameters
interface UseVirtualTableOptions<TData> {
/** The TanStack Table instance */
table: Table<TData>
/** Estimated row size in pixels, or function to calculate per row */
estimateSize: number | ((index: number) => number)
/** Number of items to render outside visible area (default: 5) */
overscan?: number
/** Custom scroll element getter */
getScrollElement?: () => HTMLElement | null
/** Custom element measurement function for dynamic heights */
measureElement?: (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer) => number
/** Custom range extractor */
rangeExtractor?: (range: Range) => number[]
}Returns
interface UseVirtualTableReturn {
/** Virtual row items to render */
rows: VirtualItem[]
/** The virtualizer instance from @tanstack/react-virtual */
virtualizer: Virtualizer<HTMLElement, Element>
/** Ref to attach to the scrollable container */
containerRef: RefObject<HTMLDivElement | null>
}Options Details
table(required): Your TanStack Table instance fromuseReactTable()estimateSize(required): Either a fixed number or a function that returns the estimated height for each row. Used for scroll calculation.// Fixed height estimateSize: 35 // Dynamic height estimateSize: (index) => { const row = table.getRowModel().rows[index] return row.original.description.length > 100 ? 80 : 40 }overscan(optional, default: 5): Number of rows to render outside the visible area. Higher values improve scroll smoothness but use more memory.getScrollElement(optional): Custom function to get the scroll container. By default, usescontainerRef.measureElement(optional): Custom measurement function for dynamic row heights. Used withvirtualizer.measureElementref callback.rangeExtractor(optional): Custom range extraction logic from TanStack Virtual.
Examples
Basic Example (Fixed Heights)
Simple table with 10,000 rows, sorting, and filtering:
const { rows, virtualizer, containerRef } = useVirtualTable({
table,
estimateSize: 35, // All rows are 35px tall
overscan: 5,
})Dynamic Heights Example
Rows with variable content and expandable sub-rows:
const { rows, virtualizer, containerRef } = useVirtualTable({
table,
estimateSize: (index) => {
const row = table.getRowModel().rows[index]
return estimateRowHeight(row) // Your custom logic
},
measureElement: virtualizer.measureElement, // Enable dynamic measurement
})Real-World Example
50,000 rows with all features (search, filter, sort, selection):
const { rows, virtualizer, containerRef } = useVirtualTable({
table,
estimateSize: 40,
overscan: 10, // Render more for smoother scrolling
})TypeScript
This package is written in TypeScript and provides full type safety:
import { useVirtualTable, UseVirtualTableOptions, UseVirtualTableReturn } from 'react-table-virtualized'
import type { Table } from '@tanstack/react-table'
// Type inference works automatically
const { rows, virtualizer, containerRef } = useVirtualTable({
table, // Table<MyDataType>
estimateSize: 35,
})
// Or explicitly type the options
const options: UseVirtualTableOptions<MyDataType> = {
table,
estimateSize: 35,
}
const result: UseVirtualTableReturn = useVirtualTable(options)Performance Tips
- Use
estimateSizewisely: More accurate estimates = smoother scrolling - Adjust
overscan: Higher values = smoother scroll but more DOM nodes - Memoize your columns: Prevents unnecessary re-renders
- Use
useMemofor data: Especially with sorting/filtering - Enable dynamic measurement only when needed: It has a small performance cost
FAQ
Does this work with sorting and filtering?
Yes! It works seamlessly with all TanStack Table features:
- ✅ Sorting
- ✅ Filtering
- ✅ Grouping
- ✅ Pagination
- ✅ Column resizing
- ✅ Row selection
- ✅ Expanding rows
- ✅ Everything else
The virtualization layer sits on top of the table state, so any changes to the table automatically reflect in the virtualized view.
Can I use dynamic row heights?
Yes! Use the measureElement option:
const { rows, virtualizer, containerRef } = useVirtualTable({
table,
estimateSize: (index) => estimateHeight(index),
measureElement: virtualizer.measureElement,
})
// In your render:
<div
ref={virtualizer.measureElement}
data-index={virtualRow.index}
style={{ position: 'absolute', transform: `translateY(${virtualRow.start}px)` }}
>
{/* Your content */}
</div>What about column virtualization?
Column virtualization is not currently supported. In practice, row virtualization solves 99% of performance problems. Most tables have < 20 columns, which is perfectly fine for the DOM.
If you have 100+ columns and need column virtualization, please open an issue!
How do I add sticky headers?
Use CSS! The package doesn't handle sticky headers because it's purely a CSS concern:
<div style={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'white' }}>
{/* Header content */}
</div>Can I scroll to a specific row?
Yes! Use the virtualizer instance:
const { virtualizer } = useVirtualTable({ table, estimateSize: 35 })
// Scroll to row index 100
virtualizer.scrollToIndex(100, { align: 'start' })Does this work with React Server Components?
The core hook uses React hooks (useRef, useMemo) so it needs to run on the client. Mark your component with 'use client':
'use client'
import { useVirtualTable } from 'react-table-virtualized'How much memory does this save?
A lot! Without virtualization, a 50,000 row table creates 50,000 DOM nodes. With virtualization, you only render what's visible (~20 rows).
Memory usage:
- Without: ~50,000 DOM nodes
- With: ~20-30 DOM nodes (depending on viewport and overscan)
That's 99.9% fewer DOM nodes!
Browser Support
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
- React 16.8+ (hooks support)
Contributing
Contributions are welcome! Please open an issue or PR.
License
MIT © Yarin Keren
Credits
Built on top of:
- TanStack Table - Headless table state management
- TanStack Virtual - Headless virtualization
Related
- @tanstack/react-table - The table state management library
- @tanstack/react-virtual - The virtualization library
- react-window - Alternative virtualization library
