Virtualized Rows

Row virtualization renders only the visible rows to the DOM, improving performance when displaying large datasets. This example uses TanStack Virtual with useVirtualizer to display 30,000 rows with dynamic row height measurement and sorting.

Row virtualization is not ideal from a UX standpoint. We recommend using Pagination combined with Filtering instead.

(0 rows)
ID
Username
Role
Account Status
Last Visited At
Visit Count
import {useEffect, useMemo, useRef, useState} from "react"

import {useVirtualizer} from "@tanstack/react-virtual"
import {ArrowDown} from "lucide-react"

import {
  type ColumnDef,
  getCoreRowModel,
  getSortedRowModel,
} from "@qualcomm-ui/core/table"
import {Icon} from "@qualcomm-ui/react/icon"
import {flexRender, Table, useReactTable} from "@qualcomm-ui/react/table"
import {clsx} from "@qualcomm-ui/utils/clsx"

import {makeUserData, type User} from "./make-data"

// This is a dynamic row height example, which is more complicated, but allows for a
// more realistic table.
export function VirtualizedRowsDemo() {
  const columns = useMemo<ColumnDef<User>[]>(
    () => [
      {
        accessorKey: "id",
        header: "ID",
        size: 60,
      },
      {
        accessorKey: "username",
        header: "Username",
        id: "username",
      },
      {
        accessorKey: "role",
        header: "Role",
        id: "role",
        size: 120,
      },
      {
        accessorKey: "accountStatus",
        header: "Account Status",
        id: "accountStatus",
      },
      {
        accessorKey: "lastVisitedAt",
        header: "Last Visited At",
        id: "lastVisitedAt",
        minSize: 205,
      },
      {
        accessorKey: "visitCount",
        header: "Visit Count",
        id: "visitCount",
      },
    ],
    [],
  )

  const [data, setData] = useState<User[]>([])

  useEffect(() => {
    setData(makeUserData(30000))
  }, [])

  const table = useReactTable({
    columns,
    data,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  })

  const {rows} = table.getRowModel()

  // The virtualizer needs to know the scrollable container element
  const tableContainerRef = useRef<HTMLDivElement>(null)

  const rowVirtualizer = useVirtualizer({
    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,
  })

  return (
    <div className="overflow-x-auto">
      ({data.length} rows)
      <div
        ref={tableContainerRef}
        className="relative mt-2 h-[800px] overflow-auto"
      >
        {/*
         * Even though we're still using semantic table tags, we must use
         * CSS grid and flexbox for dynamic row heights
         */}
        <Table.Root className="border-0">
          <Table.Table className="grid table-fixed border-1">
            <Table.Header className="bg-neutral-01 sticky top-0 z-10 grid">
              {table.getHeaderGroups().map((headerGroup) => (
                <Table.Row
                  key={headerGroup.id}
                  className="border-neutral-01 flex w-full border-b"
                >
                  {headerGroup.headers.map((header) => {
                    if (header.isPlaceholder) {
                      return (
                        <Table.HeaderCell
                          key={header.id}
                          colSpan={header.colSpan}
                          style={{width: header.getSize()}}
                        />
                      )
                    }
                    const sorted = header.column.getIsSorted()
                    return (
                      <Table.HeaderCell
                        key={header.id}
                        colSpan={header.colSpan}
                        onClick={header.column.getToggleSortingHandler()}
                        style={{width: header.getSize()}}
                      >
                        <div className="inline-flex h-full items-center gap-2">
                          {flexRender(
                            header.column.columnDef.header,
                            header.getContext(),
                          )}
                          {sorted ? (
                            <Icon
                              className={clsx(
                                "transition-[transform] ease-in-out",
                                {
                                  "rotate-180": sorted === "asc",
                                },
                              )}
                              icon={ArrowDown}
                              size="sm"
                            />
                          ) : null}
                        </div>
                      </Table.HeaderCell>
                    )
                  })}
                </Table.Row>
              ))}
            </Table.Header>
            <Table.Body
              className="relative grid"
              style={{
                height: `${rowVirtualizer.getTotalSize()}px`, // tells scrollbar how big the table is
              }}
            >
              {rowVirtualizer.getVirtualItems().map((virtualRow) => {
                const row = rows[virtualRow.index]
                return (
                  <Table.Row
                    key={row.id}
                    ref={(node) => rowVirtualizer.measureElement(node)} // measure dynamic row height
                    className="absolute flex w-full border-l-[1px]"
                    data-index={virtualRow.index} // needed for dynamic row height measurement
                    style={{
                      borderColor: "var(--q-border-1-subtle)",
                      transform: `translateY(${virtualRow.start}px)`, // this should always be a `style` as it changes on scroll
                    }}
                  >
                    {row.getVisibleCells().map((cell) => {
                      return (
                        <Table.Cell
                          key={cell.id}
                          className="flex items-center"
                          style={{
                            width: cell.column.getSize(),
                          }}
                        >
                          {flexRender(
                            cell.column.columnDef.cell,
                            cell.getContext(),
                          )}
                        </Table.Cell>
                      )
                    })}
                  </Table.Row>
                )
              })}
            </Table.Body>
          </Table.Table>
        </Table.Root>
      </div>
    </div>
  )
}