|
| 1 | +import React from 'react' |
| 2 | +import ReactDOM from 'react-dom/client' |
| 3 | + |
| 4 | +import './index.css' |
| 5 | + |
| 6 | +import { |
| 7 | + ColumnDef, |
| 8 | + flexRender, |
| 9 | + getCoreRowModel, |
| 10 | + getSortedRowModel, |
| 11 | + Row, |
| 12 | + useReactTable, |
| 13 | +} from '@tanstack/react-table' |
| 14 | + |
| 15 | +import { useVirtualizer } from '@tanstack/react-virtual' |
| 16 | + |
| 17 | +import { makeColumns, makeData, Person } from './makeData' |
| 18 | + |
| 19 | +//This is a dynamic row height example, which is more complicated, but allows for a more realistic table. |
| 20 | +//See https://tanstack.com/virtual/v3/docs/examples/react/table for a simpler fixed row height example. |
| 21 | +function App() { |
| 22 | + const columns = React.useMemo<ColumnDef<Person>[]>( |
| 23 | + () => makeColumns(1_000), |
| 24 | + [] |
| 25 | + ) |
| 26 | + |
| 27 | + const [data, _setData] = React.useState(() => makeData(1_000, columns)) |
| 28 | + |
| 29 | + const table = useReactTable({ |
| 30 | + data, |
| 31 | + columns, |
| 32 | + getCoreRowModel: getCoreRowModel(), |
| 33 | + getSortedRowModel: getSortedRowModel(), |
| 34 | + debugTable: true, |
| 35 | + }) |
| 36 | + |
| 37 | + const { rows } = table.getRowModel() |
| 38 | + |
| 39 | + //The virtualizers need to know the scrollable container element |
| 40 | + const tableContainerRef = React.useRef<HTMLDivElement>(null) |
| 41 | + |
| 42 | + //get first 16 column widths and average them for column size estimation |
| 43 | + const averageColumnWidth = React.useMemo(() => { |
| 44 | + const columnsWidths = |
| 45 | + table |
| 46 | + .getRowModel() |
| 47 | + .rows[0]?.getCenterVisibleCells() |
| 48 | + ?.slice(0, 16) |
| 49 | + ?.map(cell => cell.column.getSize()) ?? [] |
| 50 | + return columnsWidths.reduce((a, b) => a + b, 0) / columnsWidths.length |
| 51 | + }, [table.getRowModel().rows]) |
| 52 | + |
| 53 | + //we are using a slightly different virtualization strategy for columns in order to support dynamic row heights |
| 54 | + const columnVirtualizer = useVirtualizer({ |
| 55 | + count: table.getVisibleLeafColumns().length, |
| 56 | + estimateSize: () => averageColumnWidth, //average column width in pixels |
| 57 | + getScrollElement: () => tableContainerRef.current, |
| 58 | + horizontal: true, |
| 59 | + overscan: 3, |
| 60 | + }) |
| 61 | + |
| 62 | + //dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without the need for `measureElement` |
| 63 | + const rowVirtualizer = useVirtualizer({ |
| 64 | + count: rows.length, |
| 65 | + estimateSize: () => 33, //estimate row height for accurate scrollbar dragging |
| 66 | + getScrollElement: () => tableContainerRef.current, |
| 67 | + //measure dynamic row height, except in firefox because it measures table border height incorrectly |
| 68 | + measureElement: |
| 69 | + typeof window !== 'undefined' && |
| 70 | + navigator.userAgent.indexOf('Firefox') === -1 |
| 71 | + ? element => element?.getBoundingClientRect().height |
| 72 | + : undefined, |
| 73 | + overscan: 5, |
| 74 | + }) |
| 75 | + |
| 76 | + const virtualColumns = columnVirtualizer.getVirtualItems() |
| 77 | + const virtualRows = rowVirtualizer.getVirtualItems() |
| 78 | + |
| 79 | + //different virtualization strategy for columns - instead of absolute and translateY, we add empty columns to the left and right |
| 80 | + let virtualPaddingLeft: number | undefined |
| 81 | + let virtualPaddingRight: number | undefined |
| 82 | + |
| 83 | + if (columnVirtualizer && virtualColumns?.length) { |
| 84 | + virtualPaddingLeft = virtualColumns[0]?.start ?? 0 |
| 85 | + virtualPaddingRight = |
| 86 | + columnVirtualizer.getTotalSize() - |
| 87 | + (virtualColumns[virtualColumns.length - 1]?.end ?? 0) |
| 88 | + } |
| 89 | + |
| 90 | + //All important CSS styles are included as inline styles for this example. This is not recommended for your code. |
| 91 | + return ( |
| 92 | + <div className="app"> |
| 93 | + {process.env.NODE_ENV === 'development' ? ( |
| 94 | + <p> |
| 95 | + <strong>Notice:</strong> You are currently running React in |
| 96 | + development mode. Rendering performance will be slightly degraded |
| 97 | + until this application is built for production. |
| 98 | + </p> |
| 99 | + ) : null} |
| 100 | + <div>({columns.length.toLocaleString()} columns)</div> |
| 101 | + <div>({data.length.toLocaleString()} rows)</div> |
| 102 | + |
| 103 | + <div |
| 104 | + className="container" |
| 105 | + ref={tableContainerRef} |
| 106 | + style={{ |
| 107 | + overflow: 'auto', //our scrollable table container |
| 108 | + position: 'relative', //needed for sticky header |
| 109 | + height: '800px', //should be a fixed height |
| 110 | + }} |
| 111 | + > |
| 112 | + {/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */} |
| 113 | + <table style={{ display: 'grid' }}> |
| 114 | + <thead |
| 115 | + style={{ |
| 116 | + display: 'grid', |
| 117 | + position: 'sticky', |
| 118 | + top: 0, |
| 119 | + zIndex: 1, |
| 120 | + }} |
| 121 | + > |
| 122 | + {table.getHeaderGroups().map(headerGroup => ( |
| 123 | + <tr |
| 124 | + key={headerGroup.id} |
| 125 | + style={{ display: 'flex', width: '100%' }} |
| 126 | + > |
| 127 | + {virtualPaddingLeft ? ( |
| 128 | + //fake empty column to the left for virtualization scroll padding |
| 129 | + <th style={{ display: 'flex', width: virtualPaddingLeft }} /> |
| 130 | + ) : null} |
| 131 | + {virtualColumns.map(vc => { |
| 132 | + const header = headerGroup.headers[vc.index] |
| 133 | + return ( |
| 134 | + <th |
| 135 | + key={header.id} |
| 136 | + style={{ |
| 137 | + display: 'flex', |
| 138 | + width: header.getSize(), |
| 139 | + }} |
| 140 | + > |
| 141 | + <div |
| 142 | + {...{ |
| 143 | + className: header.column.getCanSort() |
| 144 | + ? 'cursor-pointer select-none' |
| 145 | + : '', |
| 146 | + onClick: header.column.getToggleSortingHandler(), |
| 147 | + }} |
| 148 | + > |
| 149 | + {flexRender( |
| 150 | + header.column.columnDef.header, |
| 151 | + header.getContext() |
| 152 | + )} |
| 153 | + {{ |
| 154 | + asc: ' 🔼', |
| 155 | + desc: ' 🔽', |
| 156 | + }[header.column.getIsSorted() as string] ?? null} |
| 157 | + </div> |
| 158 | + </th> |
| 159 | + ) |
| 160 | + })} |
| 161 | + {virtualPaddingRight ? ( |
| 162 | + //fake empty column to the right for virtualization scroll padding |
| 163 | + <th style={{ display: 'flex', width: virtualPaddingRight }} /> |
| 164 | + ) : null} |
| 165 | + </tr> |
| 166 | + ))} |
| 167 | + </thead> |
| 168 | + <tbody |
| 169 | + style={{ |
| 170 | + display: 'grid', |
| 171 | + height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is |
| 172 | + position: 'relative', //needed for absolute positioning of rows |
| 173 | + }} |
| 174 | + > |
| 175 | + {virtualRows.map(virtualRow => { |
| 176 | + const row = rows[virtualRow.index] as Row<Person> |
| 177 | + const visibleCells = row.getVisibleCells() |
| 178 | + |
| 179 | + return ( |
| 180 | + <tr |
| 181 | + data-index={virtualRow.index} //needed for dynamic row height measurement |
| 182 | + ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height |
| 183 | + key={row.id} |
| 184 | + style={{ |
| 185 | + display: 'flex', |
| 186 | + position: 'absolute', |
| 187 | + transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll |
| 188 | + width: '100%', |
| 189 | + }} |
| 190 | + > |
| 191 | + {virtualPaddingLeft ? ( |
| 192 | + //fake empty column to the left for virtualization scroll padding |
| 193 | + <td |
| 194 | + style={{ display: 'flex', width: virtualPaddingLeft }} |
| 195 | + /> |
| 196 | + ) : null} |
| 197 | + {virtualColumns.map(vc => { |
| 198 | + const cell = visibleCells[vc.index] |
| 199 | + return ( |
| 200 | + <td |
| 201 | + key={cell.id} |
| 202 | + style={{ |
| 203 | + display: 'flex', |
| 204 | + width: cell.column.getSize(), |
| 205 | + }} |
| 206 | + > |
| 207 | + {flexRender( |
| 208 | + cell.column.columnDef.cell, |
| 209 | + cell.getContext() |
| 210 | + )} |
| 211 | + </td> |
| 212 | + ) |
| 213 | + })} |
| 214 | + {virtualPaddingRight ? ( |
| 215 | + //fake empty column to the right for virtualization scroll padding |
| 216 | + <td |
| 217 | + style={{ display: 'flex', width: virtualPaddingRight }} |
| 218 | + /> |
| 219 | + ) : null} |
| 220 | + </tr> |
| 221 | + ) |
| 222 | + })} |
| 223 | + </tbody> |
| 224 | + </table> |
| 225 | + </div> |
| 226 | + </div> |
| 227 | + ) |
| 228 | +} |
| 229 | + |
| 230 | +const rootElement = document.getElementById('root') |
| 231 | + |
| 232 | +if (!rootElement) throw new Error('Failed to find the root element') |
| 233 | + |
| 234 | +ReactDOM.createRoot(rootElement).render( |
| 235 | + <React.StrictMode> |
| 236 | + <App /> |
| 237 | + </React.StrictMode> |
| 238 | +) |
0 commit comments