import React, { useEffect } from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import {
columnSizingFeature,
createSortedRowModel,
rowSortingFeature,
sortFns,
useTable,
} from '@tanstack/react-table'
import { useVirtualizer } from '@tanstack/react-virtual'
import { makeData } from './makeData'
import type { ColumnDef, ReactTable, Row } from '@tanstack/react-table'
import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'
import type { Person } from './makeData'
const features = {
columnSizingFeature,
rowSortingFeature,
}
// This is a dynamic row height example, which is more complicated, but allows for a more realistic table.
// See https://tanstack.com/virtual/v3/docs/examples/react/table for a simpler fixed row height example.
function App() {
const columns = React.useMemo<Array<ColumnDef<typeof features, Person>>>(
() => [
{
accessorKey: 'id',
header: 'ID',
size: 60,
},
{
accessorKey: 'firstName',
cell: (info) => info.getValue(),
},
{
accessorFn: (row) => row.lastName,
id: 'lastName',
cell: (info) => info.getValue(),
header: () => <span>Last Name</span>,
},
{
accessorKey: 'age',
header: () => 'Age',
size: 50,
},
{
accessorKey: 'visits',
header: () => <span>Visits</span>,
size: 50,
},
{
accessorKey: 'status',
header: 'Status',
},
{
accessorKey: 'progress',
header: 'Profile Progress',
size: 80,
},
{
accessorKey: 'createdAt',
header: 'Created At',
cell: (info) => info.getValue<Date>().toLocaleString(),
size: 250,
},
],
[],
)
// The virtualizer will need a reference to the scrollable container element
const tableContainerRef = React.useRef<HTMLDivElement>(null)
const [data, setData] = React.useState(() => makeData(100_000))
const refreshData = React.useCallback(() => {
setData(makeData(100_000))
}, [])
const table = useTable({
_features: features,
_rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
columns,
data,
debugTable: true,
})
// All important CSS styles are included as inline styles for this example. This is not recommended for your code.
return (
<>
<div className="app">
{process.env.NODE_ENV === 'development' ? (
<p>
<strong>Notice:</strong> You are currently running React in
development mode. Virtualized rendering performance will be slightly
degraded until this application is built for production.
</p>
) : null}
({data.length.toLocaleString()} rows)
<button onClick={refreshData}>Refresh Data</button>
<div
className="container"
ref={tableContainerRef}
style={{
overflow: 'auto', // our scrollable table container
position: 'relative', // needed for sticky header
height: '800px', // should be a fixed height
}}
>
<table.Subscribe selector={(state) => state}>
{() => (
// Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights
<table style={{ display: 'grid' }}>
<thead
style={{
display: 'grid',
position: 'sticky',
top: 0,
zIndex: 1,
}}
>
{table.getHeaderGroups().map((headerGroup) => (
<tr
key={headerGroup.id}
style={{ display: 'flex', width: '100%' }}
>
{headerGroup.headers.map((header) => {
return (
<th
key={header.id}
style={{
display: 'flex',
width: header.getSize(),
}}
>
<div
{...{
className: header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
onClick:
header.column.getToggleSortingHandler(),
}}
>
<table.FlexRender header={header} />
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
)
})}
</tr>
))}
</thead>
<TableBody
table={table}
tableContainerRef={tableContainerRef}
/>
</table>
)}
</table.Subscribe>
</div>
</div>
<table.Subscribe selector={(state) => state}>
{(state) => <pre>{JSON.stringify(state, null, 2)}</pre>}
</table.Subscribe>
</>
)
}
interface TableBodyProps {
table: ReactTable<typeof features, Person>
tableContainerRef: React.RefObject<HTMLDivElement | null>
}
function TableBody({ table, tableContainerRef }: TableBodyProps) {
const { rows } = table.getRowModel()
// Important: Keep the row virtualizer in the lowest component possible to avoid unnecessary re-renders.
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => 33, // estimate row height for accurate scrollbar dragging
getScrollElement: () => tableContainerRef.current,
// measure dynamic row height, except in firefox because it measures table border height incorrectly
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? (element) => element.getBoundingClientRect().height
: undefined,
overscan: 5,
})
useEffect(() => {
rowVirtualizer.measure()
}, [])
return (
<tbody
style={{
display: 'grid',
height: `${rowVirtualizer.getTotalSize()}px`, // tells scrollbar how big the table is
position: 'relative', // needed for absolute positioning of rows
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<TableBodyRow
key={row.id}
row={row}
virtualRow={virtualRow}
rowVirtualizer={rowVirtualizer}
table={table}
/>
)
})}
</tbody>
)
}
interface TableBodyRowProps {
row: Row<typeof features, Person>
virtualRow: VirtualItem
rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>
table: ReactTable<typeof features, Person>
}
function TableBodyRow({
row,
virtualRow,
rowVirtualizer,
table,
}: TableBodyRowProps) {
return (
<tr
data-index={virtualRow.index} // needed for dynamic row height measurement
ref={(node) => rowVirtualizer.measureElement(node)} // measure dynamic row height
key={row.id}
style={{
display: 'flex',
position: 'absolute',
transform: `translateY(${virtualRow.start}px)`, // this should always be a `style` as it changes on scroll
width: '100%',
}}
>
{row.getAllCells().map((cell) => {
return (
<td
key={cell.id}
style={{
display: 'flex',
width: cell.column.getSize(),
}}
>
<table.FlexRender cell={cell} />
</td>
)
})}
</tr>
)
}
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)