import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' import { Cell, ColumnDef, Header, HeaderGroup, Row, Table, flexRender, getCoreRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table' import { useVirtualizer, VirtualItem, Virtualizer, } from '@tanstack/react-virtual' import { makeColumns, makeData, Person } from './makeData' function App() { const columns = React.useMemo<ColumnDef<Person>[]>( () => makeColumns(1_000), [] ) const [data, setData] = React.useState(() => makeData(1_000, columns)) const refreshData = React.useCallback(() => { setData(makeData(1_000, columns)) }, [columns]) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), 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} <div>({columns.length.toLocaleString()} columns)</div> <div>({data.length.toLocaleString()} rows)</div> <button onClick={refreshData}>Refresh Data</button> <TableContainer table={table} /> </div> ) } interface TableContainerProps { table: Table<Person> } function TableContainer({ table }: TableContainerProps) { const visibleColumns = table.getVisibleLeafColumns() //The virtualizers need to know the scrollable container element const tableContainerRef = React.useRef<HTMLDivElement>(null) //we are using a slightly different virtualization strategy for columns (compared to virtual rows) in order to support dynamic row heights const columnVirtualizer = useVirtualizer< HTMLDivElement, HTMLTableCellElement >({ count: visibleColumns.length, estimateSize: index => visibleColumns[index].getSize(), //estimate width of each column for accurate scrollbar dragging getScrollElement: () => tableContainerRef.current, horizontal: true, overscan: 3, //how many columns to render on each side off screen each way (adjust this for performance) }) const virtualColumns = columnVirtualizer.getVirtualItems() //different virtualization strategy for columns - instead of absolute and translateY, we add empty columns to the left and right let virtualPaddingLeft: number | undefined let virtualPaddingRight: number | undefined if (columnVirtualizer && virtualColumns?.length) { virtualPaddingLeft = virtualColumns[0]?.start ?? 0 virtualPaddingRight = columnVirtualizer.getTotalSize() - (virtualColumns[virtualColumns.length - 1]?.end ?? 0) } return ( <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 }} > {/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */} <table style={{ display: 'grid' }}> <TableHead columnVirtualizer={columnVirtualizer} table={table} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} /> <TableBody columnVirtualizer={columnVirtualizer} table={table} tableContainerRef={tableContainerRef} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} /> </table> </div> ) } interface TableHeadProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> table: Table<Person> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined } function TableHead({ columnVirtualizer, table, virtualPaddingLeft, virtualPaddingRight, }: TableHeadProps) { return ( <thead style={{ display: 'grid', position: 'sticky', top: 0, zIndex: 1, }} > {table.getHeaderGroups().map(headerGroup => ( <TableHeadRow columnVirtualizer={columnVirtualizer} headerGroup={headerGroup} key={headerGroup.id} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} /> ))} </thead> ) } interface TableHeadRowProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> headerGroup: HeaderGroup<Person> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined } function TableHeadRow({ columnVirtualizer, headerGroup, virtualPaddingLeft, virtualPaddingRight, }: TableHeadRowProps) { const virtualColumns = columnVirtualizer.getVirtualItems() return ( <tr key={headerGroup.id} style={{ display: 'flex', width: '100%' }}> {virtualPaddingLeft ? ( //fake empty column to the left for virtualization scroll padding <th style={{ display: 'flex', width: virtualPaddingLeft }} /> ) : null} {virtualColumns.map(virtualColumn => { const header = headerGroup.headers[virtualColumn.index] return <TableHeadCell key={header.id} header={header} /> })} {virtualPaddingRight ? ( //fake empty column to the right for virtualization scroll padding <th style={{ display: 'flex', width: virtualPaddingRight }} /> ) : null} </tr> ) } interface TableHeadCellProps { header: Header<Person, unknown> } function TableHeadCell({ header }: TableHeadCellProps) { return ( <th key={header.id} style={{ display: 'flex', width: header.getSize(), }} > <div {...{ className: header.column.getCanSort() ? 'cursor-pointer select-none' : '', onClick: header.column.getToggleSortingHandler(), }} > {flexRender(header.column.columnDef.header, header.getContext())} {{ asc: ' 🔼', desc: ' 🔽', }[header.column.getIsSorted() as string] ?? null} </div> </th> ) } interface TableBodyProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> table: Table<Person> tableContainerRef: React.RefObject<HTMLDivElement> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined } function TableBody({ columnVirtualizer, table, tableContainerRef, virtualPaddingLeft, virtualPaddingRight, }: TableBodyProps) { const { rows } = table.getRowModel() //dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without the need for `measureElement` 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, }) const virtualRows = rowVirtualizer.getVirtualItems() return ( <tbody style={{ display: 'grid', height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is position: 'relative', //needed for absolute positioning of rows }} > {virtualRows.map(virtualRow => { const row = rows[virtualRow.index] as Row<Person> return ( <TableBodyRow columnVirtualizer={columnVirtualizer} key={row.id} row={row} rowVirtualizer={rowVirtualizer} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} virtualRow={virtualRow} /> ) })} </tbody> ) } interface TableBodyRowProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> row: Row<Person> rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined virtualRow: VirtualItem } function TableBodyRow({ columnVirtualizer, row, rowVirtualizer, virtualPaddingLeft, virtualPaddingRight, virtualRow, }: TableBodyRowProps) { const visibleCells = row.getVisibleCells() const virtualColumns = columnVirtualizer.getVirtualItems() 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%', }} > {virtualPaddingLeft ? ( //fake empty column to the left for virtualization scroll padding <td style={{ display: 'flex', width: virtualPaddingLeft }} /> ) : null} {virtualColumns.map(vc => { const cell = visibleCells[vc.index] return <TableBodyCell key={cell.id} cell={cell} /> })} {virtualPaddingRight ? ( //fake empty column to the right for virtualization scroll padding <td style={{ display: 'flex', width: virtualPaddingRight }} /> ) : null} </tr> ) } interface TableBodyCellProps { cell: Cell<Person, unknown> } function TableBodyCell({ cell }: TableBodyCellProps) { return ( <td key={cell.id} style={{ display: 'flex', width: cell.column.getSize(), }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ) } 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> )
import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' import { Cell, ColumnDef, Header, HeaderGroup, Row, Table, flexRender, getCoreRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table' import { useVirtualizer, VirtualItem, Virtualizer, } from '@tanstack/react-virtual' import { makeColumns, makeData, Person } from './makeData' function App() { const columns = React.useMemo<ColumnDef<Person>[]>( () => makeColumns(1_000), [] ) const [data, setData] = React.useState(() => makeData(1_000, columns)) const refreshData = React.useCallback(() => { setData(makeData(1_000, columns)) }, [columns]) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), 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} <div>({columns.length.toLocaleString()} columns)</div> <div>({data.length.toLocaleString()} rows)</div> <button onClick={refreshData}>Refresh Data</button> <TableContainer table={table} /> </div> ) } interface TableContainerProps { table: Table<Person> } function TableContainer({ table }: TableContainerProps) { const visibleColumns = table.getVisibleLeafColumns() //The virtualizers need to know the scrollable container element const tableContainerRef = React.useRef<HTMLDivElement>(null) //we are using a slightly different virtualization strategy for columns (compared to virtual rows) in order to support dynamic row heights const columnVirtualizer = useVirtualizer< HTMLDivElement, HTMLTableCellElement >({ count: visibleColumns.length, estimateSize: index => visibleColumns[index].getSize(), //estimate width of each column for accurate scrollbar dragging getScrollElement: () => tableContainerRef.current, horizontal: true, overscan: 3, //how many columns to render on each side off screen each way (adjust this for performance) }) const virtualColumns = columnVirtualizer.getVirtualItems() //different virtualization strategy for columns - instead of absolute and translateY, we add empty columns to the left and right let virtualPaddingLeft: number | undefined let virtualPaddingRight: number | undefined if (columnVirtualizer && virtualColumns?.length) { virtualPaddingLeft = virtualColumns[0]?.start ?? 0 virtualPaddingRight = columnVirtualizer.getTotalSize() - (virtualColumns[virtualColumns.length - 1]?.end ?? 0) } return ( <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 }} > {/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */} <table style={{ display: 'grid' }}> <TableHead columnVirtualizer={columnVirtualizer} table={table} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} /> <TableBody columnVirtualizer={columnVirtualizer} table={table} tableContainerRef={tableContainerRef} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} /> </table> </div> ) } interface TableHeadProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> table: Table<Person> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined } function TableHead({ columnVirtualizer, table, virtualPaddingLeft, virtualPaddingRight, }: TableHeadProps) { return ( <thead style={{ display: 'grid', position: 'sticky', top: 0, zIndex: 1, }} > {table.getHeaderGroups().map(headerGroup => ( <TableHeadRow columnVirtualizer={columnVirtualizer} headerGroup={headerGroup} key={headerGroup.id} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} /> ))} </thead> ) } interface TableHeadRowProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> headerGroup: HeaderGroup<Person> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined } function TableHeadRow({ columnVirtualizer, headerGroup, virtualPaddingLeft, virtualPaddingRight, }: TableHeadRowProps) { const virtualColumns = columnVirtualizer.getVirtualItems() return ( <tr key={headerGroup.id} style={{ display: 'flex', width: '100%' }}> {virtualPaddingLeft ? ( //fake empty column to the left for virtualization scroll padding <th style={{ display: 'flex', width: virtualPaddingLeft }} /> ) : null} {virtualColumns.map(virtualColumn => { const header = headerGroup.headers[virtualColumn.index] return <TableHeadCell key={header.id} header={header} /> })} {virtualPaddingRight ? ( //fake empty column to the right for virtualization scroll padding <th style={{ display: 'flex', width: virtualPaddingRight }} /> ) : null} </tr> ) } interface TableHeadCellProps { header: Header<Person, unknown> } function TableHeadCell({ header }: TableHeadCellProps) { return ( <th key={header.id} style={{ display: 'flex', width: header.getSize(), }} > <div {...{ className: header.column.getCanSort() ? 'cursor-pointer select-none' : '', onClick: header.column.getToggleSortingHandler(), }} > {flexRender(header.column.columnDef.header, header.getContext())} {{ asc: ' 🔼', desc: ' 🔽', }[header.column.getIsSorted() as string] ?? null} </div> </th> ) } interface TableBodyProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> table: Table<Person> tableContainerRef: React.RefObject<HTMLDivElement> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined } function TableBody({ columnVirtualizer, table, tableContainerRef, virtualPaddingLeft, virtualPaddingRight, }: TableBodyProps) { const { rows } = table.getRowModel() //dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without the need for `measureElement` 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, }) const virtualRows = rowVirtualizer.getVirtualItems() return ( <tbody style={{ display: 'grid', height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is position: 'relative', //needed for absolute positioning of rows }} > {virtualRows.map(virtualRow => { const row = rows[virtualRow.index] as Row<Person> return ( <TableBodyRow columnVirtualizer={columnVirtualizer} key={row.id} row={row} rowVirtualizer={rowVirtualizer} virtualPaddingLeft={virtualPaddingLeft} virtualPaddingRight={virtualPaddingRight} virtualRow={virtualRow} /> ) })} </tbody> ) } interface TableBodyRowProps { columnVirtualizer: Virtualizer<HTMLDivElement, HTMLTableCellElement> row: Row<Person> rowVirtualizer: Virtualizer<HTMLDivElement, HTMLTableRowElement> virtualPaddingLeft: number | undefined virtualPaddingRight: number | undefined virtualRow: VirtualItem } function TableBodyRow({ columnVirtualizer, row, rowVirtualizer, virtualPaddingLeft, virtualPaddingRight, virtualRow, }: TableBodyRowProps) { const visibleCells = row.getVisibleCells() const virtualColumns = columnVirtualizer.getVirtualItems() 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%', }} > {virtualPaddingLeft ? ( //fake empty column to the left for virtualization scroll padding <td style={{ display: 'flex', width: virtualPaddingLeft }} /> ) : null} {virtualColumns.map(vc => { const cell = visibleCells[vc.index] return <TableBodyCell key={cell.id} cell={cell} /> })} {virtualPaddingRight ? ( //fake empty column to the right for virtualization scroll padding <td style={{ display: 'flex', width: virtualPaddingRight }} /> ) : null} </tr> ) } interface TableBodyCellProps { cell: Cell<Person, unknown> } function TableBodyCell({ cell }: TableBodyCellProps) { return ( <td key={cell.id} style={{ display: 'flex', width: cell.column.getSize(), }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ) } 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> )
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.