React Example: Virtualized Rows

import React, { useEffect } from 'react'
import ReactDOM from 'react-dom/client'

import './index.css'

import {
  columnSizingFeature,
  createColumnHelper,
  createSortedRowModel,
  rowSortingFeature,
  sortFns,
  useTable,
} from '@tanstack/react-table'
import { useVirtualizer } from '@tanstack/react-virtual'
import { makeData } from './makeData'
import type { ReactTable, Row } from '@tanstack/react-table'
import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'
import type { Person } from './makeData'

const features = {
  columnSizingFeature,
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns,
}

const columnHelper = createColumnHelper<typeof features, Person>()
// 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(
    () =>
      columnHelper.columns([
        columnHelper.accessor('id', {
          header: 'ID',
          size: 60,
        }),
        columnHelper.accessor('firstName', {
          cell: (info) => info.getValue(),
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          cell: (info) => info.getValue(),
          header: () => <span>Last Name</span>,
        }),
        columnHelper.accessor('age', {
          header: () => 'Age',
          size: 50,
        }),
        columnHelper.accessor('visits', {
          header: () => <span>Visits</span>,
          size: 50,
        }),
        columnHelper.accessor('status', {
          header: 'Status',
        }),
        columnHelper.accessor('progress', {
          header: 'Profile Progress',
          size: 80,
        }),
        columnHelper.accessor('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(200_000))

  const refreshData = React.useCallback(() => {
    setData(makeData(200_000))
  }, [])

  const stressTest = React.useCallback(() => {
    setData(makeData(1_000_000))
  }, [])

  const table = useTable(
    {
      features,
      columns,
      data,
      debugTable: true,
    },
    (state) => state, // default selector
  )

  // 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)
        <div>
          <button onClick={refreshData}>Regenerate Data</button>
          <button onClick={stressTest}>Stress Test (1M rows)</button>
        </div>
        <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' }}>
            <thead
              style={{
                display: 'grid',
                height: '34px',
                position: 'sticky',
                top: 0,
                zIndex: 1,
              }}
            >
              {table.getHeaderGroups().map((headerGroup) => (
                <tr
                  key={headerGroup.id}
                  style={{ display: 'flex', height: '34px', width: '100%' }}
                >
                  {headerGroup.headers.map((header) => {
                    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(),
                          }}
                        >
                          <table.FlexRender header={header} />
                          {{
                            asc: ' 🔼',
                            desc: ' 🔽',
                          }[header.column.getIsSorted() as string] ?? null}
                        </div>
                      </th>
                    )
                  })}
                </tr>
              ))}
            </thead>
            <TableBody table={table} tableContainerRef={tableContainerRef} />
          </table>
        </div>
      </div>
      <pre>{JSON.stringify(table.state, null, 2)}</pre>
    </>
  )
}

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>,
)