import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import {
FlexRender,
columnSizingFeature,
createColumnHelper,
createSortedRowModel,
rowSortingFeature,
sortFns,
useTable,
} from '@tanstack/react-table'
import { useVirtualizer } from '@tanstack/react-virtual'
import { makeColumns, makeData } from './makeData'
import type {
Cell,
Header,
HeaderGroup,
ReactTable,
Row,
} from '@tanstack/react-table'
import type { Virtualizer } from '@tanstack/react-virtual'
import type { Person } from './makeData'
const features = {
columnSizingFeature,
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
}
const columnHelper = createColumnHelper<typeof features, Person>()
const DEFAULT_ROW_COUNT = 1_000
const DEFAULT_COLUMN_COUNT = 1_000
const STRESS_ROW_COUNT = 10_000
const STRESS_COLUMN_COUNT = 10_000
const makeTableColumns = (columnCount: number) =>
columnHelper.columns(
makeColumns(columnCount).map((column) =>
columnHelper.accessor(column.accessorKey, {
header: column.header,
size: column.size,
}),
),
)
function App() {
const [columns, setColumns] = React.useState(() =>
makeTableColumns(DEFAULT_COLUMN_COUNT),
)
const [data, setData] = React.useState(() =>
makeData(DEFAULT_ROW_COUNT, columns),
)
const refreshData = React.useCallback(() => {
const nextColumns = makeTableColumns(DEFAULT_COLUMN_COUNT)
setColumns(nextColumns)
setData(makeData(DEFAULT_ROW_COUNT, nextColumns))
}, [])
const stressTestRows = React.useCallback(() => {
setData(makeData(STRESS_ROW_COUNT, columns))
}, [columns])
const stressTestColumns = React.useCallback(() => {
const nextColumns = makeTableColumns(STRESS_COLUMN_COUNT)
setColumns(nextColumns)
setData(makeData(data.length, nextColumns))
}, [data.length])
const table = useTable(
{
features,
columns,
data,
debugTable: true,
},
(state) => state,
)
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}
<div>({columns.length.toLocaleString()} columns)</div>
<div>({data.length.toLocaleString()} rows)</div>
<div>
<button onClick={refreshData}>Regenerate Data</button>
<button onClick={stressTestRows}>Stress Test (10k rows)</button>
<button onClick={stressTestColumns}>Stress Test (10k columns)</button>
</div>
<TableContainer table={table} />
</div>
)
}
interface TableContainerProps {
table: ReactTable<typeof features, Person>
}
function TableContainer({ table }: TableContainerProps) {
const visibleColumns = table.getAllLeafColumns()
const tableContainerRef = React.useRef<HTMLDivElement>(null)
const columnVirtualizer = useVirtualizer<
HTMLDivElement,
HTMLTableCellElement
>({
count: visibleColumns.length,
estimateSize: (index) => visibleColumns[index].getSize(),
getScrollElement: () => tableContainerRef.current,
horizontal: true,
overscan: 3,
onChange: (instance) => {
const virtualColumns = instance.getVirtualItems()
const virtualPaddingLeft = virtualColumns[0]?.start ?? 0
const virtualPaddingRight =
instance.getTotalSize() -
(virtualColumns[virtualColumns.length - 1]?.end ?? 0)
tableContainerRef.current?.style.setProperty(
'--virtual-padding-left',
`${virtualPaddingLeft}px`,
)
tableContainerRef.current?.style.setProperty(
'--virtual-padding-right',
`${virtualPaddingRight}px`,
)
},
})
return (
<div
className="container"
ref={tableContainerRef}
style={{
overflow: 'auto',
position: 'relative',
height: '800px',
}}
>
{}
<table style={{ display: 'grid' }}>
<TableHead table={table} columnVirtualizer={columnVirtualizer} />
<TableBody
columnVirtualizer={columnVirtualizer}
table={table}
tableContainerRef={tableContainerRef}
/>
</table>
</div>
)
}
interface TableHeadProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
table: ReactTable<typeof features, Person>
}
function TableHead({ table, columnVirtualizer }: TableHeadProps) {
return (
<thead
style={{
display: 'grid',
height: '34px',
position: 'sticky',
top: 0,
zIndex: 1,
}}
>
{table.getHeaderGroups().map((headerGroup) => (
<TableHeadRow
columnVirtualizer={columnVirtualizer}
key={headerGroup.id}
headerGroup={headerGroup}
/>
))}
</thead>
)
}
interface TableHeadRowProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
headerGroup: HeaderGroup<typeof features, Person>
}
function TableHeadRow({ columnVirtualizer, headerGroup }: TableHeadRowProps) {
const virtualColumnIndexes = columnVirtualizer.getVirtualIndexes()
return (
<tr
key={headerGroup.id}
style={{ display: 'flex', height: '34px', width: '100%' }}
>
{}
<th className="left-column-spacer" />
{virtualColumnIndexes.map((virtualColumnIndex) => {
const header = headerGroup.headers[virtualColumnIndex]
return (
<TableHeadCellMemo
columnVirtualizer={columnVirtualizer}
key={header.id}
header={header}
/>
)
})}
{}
<th className="right-column-spacer" />
</tr>
)
}
interface TableHeadCellProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
header: Header<typeof features, Person, unknown>
}
function TableHeadCell({
columnVirtualizer: _columnVirtualizer,
header,
}: TableHeadCellProps) {
return (
<th
key={header.id}
style={{
alignItems: 'center',
display: 'flex',
height: '34px',
width: header.getSize(),
}}
>
<div
className={header.column.getCanSort() ? 'sortable-header' : ''}
onClick={header.column.getToggleSortingHandler()}
title={
header.column.getCanSort()
? header.column.getNextSortingOrder() === 'asc'
? 'Sort ascending'
: header.column.getNextSortingOrder() === 'desc'
? 'Sort descending'
: 'Clear sort'
: undefined
}
>
<FlexRender header={header} />
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
)
}
const TableHeadCellMemo = React.memo(
TableHeadCell,
(_prev, next) => next.columnVirtualizer.isScrolling,
) as typeof TableHeadCell
interface TableBodyProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
table: ReactTable<typeof features, Person>
tableContainerRef: React.RefObject<HTMLDivElement | null>
}
function TableBody({
columnVirtualizer,
table,
tableContainerRef,
}: TableBodyProps) {
const tableBodyRef = React.useRef<HTMLTableSectionElement>(null)
const rowRefsMap = React.useRef<Map<number, HTMLTableRowElement>>(new Map())
const { rows } = table.getRowModel()
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => 33,
getScrollElement: () => tableContainerRef.current,
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? (element) => element.getBoundingClientRect().height
: undefined,
overscan: 5,
onChange: (instance) => {
tableBodyRef.current!.style.height = `${instance.getTotalSize()}px`
instance.getVirtualItems().forEach((virtualRow) => {
const rowRef = rowRefsMap.current.get(virtualRow.index)
if (!rowRef) return
rowRef.style.transform = `translateY(${virtualRow.start}px)`
})
},
})
React.useLayoutEffect(() => {
rowVirtualizer.measure()
}, [table.state])
const virtualRowIndexes = rowVirtualizer.getVirtualIndexes()
return (
<tbody
ref={tableBodyRef}
style={{
display: 'grid',
position: 'relative',
}}
>
{virtualRowIndexes.map((virtualRowIndex) => {
const row = rows[virtualRowIndex]
return (
<TableBodyRow
columnVirtualizer={columnVirtualizer}
key={row.id}
row={row}
rowRefsMap={rowRefsMap}
rowVirtualizer={rowVirtualizer}
virtualRowIndex={virtualRowIndex}
/>
)
})}
</tbody>
)
}
interface TableBodyRowProps {
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
row: Row<typeof features, Person>
rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement>
virtualRowIndex: number
rowRefsMap: React.RefObject<Map<number, HTMLTableRowElement>>
}
function TableBodyRow({
columnVirtualizer,
row,
rowVirtualizer,
virtualRowIndex,
rowRefsMap,
}: TableBodyRowProps) {
const visibleCells = row.getAllCells()
const virtualColumnIndexes = columnVirtualizer.getVirtualIndexes()
return (
<tr
data-index={virtualRowIndex}
ref={(node) => {
if (node && typeof virtualRowIndex !== 'undefined') {
rowVirtualizer.measureElement(node)
rowRefsMap.current.set(virtualRowIndex, node)
}
}}
key={row.id}
style={{
display: 'flex',
position: 'absolute',
width: '100%',
}}
>
{}
<td className="left-column-spacer" />
{virtualColumnIndexes.map((virtualColumnIndex) => {
const cell = visibleCells[virtualColumnIndex]
return (
<TableBodyCellMemo
key={cell.id}
cell={cell}
columnVirtualizer={columnVirtualizer}
/>
)
})}
{}
<td className="right-column-spacer" />
</tr>
)
}
// const TableBodyRowMemo = React.memo(
interface TableBodyCellProps {
cell: Cell<typeof features, Person, unknown>
columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement>
}
function TableBodyCell({
cell,
columnVirtualizer: _columnVirtualizer,
}: TableBodyCellProps) {
return (
<td
key={cell.id}
style={{
display: 'flex',
width: cell.column.getSize(),
}}
>
<FlexRender cell={cell} />
</td>
)
}
const TableBodyCellMemo = React.memo(
TableBodyCell,
(_prev, next) => next.columnVirtualizer.isScrolling,
) as typeof TableBodyCell
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>,
)