DEV Community

Cover image for Let's create Data Table. Part 3: Virtualization
Dima Vyshniakov
Dima Vyshniakov

Posted on • Edited on

1 1 2 1

Let's create Data Table. Part 3: Virtualization

This is an article from the series about creating of an advanced Data table component using React, TanStack Table 8, Tailwind CSS and Headless UI.

In the previous article, we implemented a Data table capable to display various data formats using TanStack table.

We tested our table with a dataset of 100 rows. But what if we increase this number to 1,000 or even 10,000? Eventually, we will hit the browser's rendering limitations. The ability to display and operate on large datasets is crucial for the Data Table. That's why we will implement render virtualization.

What is virtualization?

Virtualization is a technique used to improve the performance of rendering large datasets by only rendering the visible rows and a small buffer around them. This reduces the number of DOM elements and improves the overall performance and responsiveness of the table.

Demo of virtualized scrolling

Here is the demo of Data table with more than 30 thousand rows scrolling. We are going to use @tanstack/react-virtual library to implement this.

Implement virtual table

To keep code organized, we will create folder src/DataTable/features. Here will be the place for the logic responsible for Data table features. We will implement this as React hooks.

Virtualizer React hook

Here is a custom hook using TanStack virtualizer. We also create a workaround for sticky header compatibility.

The provided code defines a custom hook src/DataTable/features/useVirtualRows.ts that leverages the @tanstack/react-virtual library to efficiently render a huge number of rows in a table by virtualizing them. This helps improve performance by only rendering the rows that are visible in the viewport, plus a few extra rows for smooth scrolling.

CELL_HEIGHT constant defines the height of each row in pixels. OVERSCAN sets the number of rows to render before and after the viewport.

The hook accepts the following parameters: rowsCount: the total number of rows, and scrollRef: a reference to the table container element that has scroll.

We also calculate before and after values to handle the space above the first virtual row and below the last virtual row. These values help resolve issues with sticky table header rendering.

import { notUndefined, useVirtualizer } from '@tanstack/react-virtual';
import type { MutableRefObject } from 'react';

const CELL_HEIGHT = 31;

const OVERSCAN = 6;

export type Props = {
  rowsCount: number;
  scrollRef: MutableRefObject<HTMLElement | null>;
};

export const useVirtualRows = ({ rowsCount, scrollRef }: Props) => {
  const virtualizer = useVirtualizer({
    count: rowsCount,
    getScrollElement: () => scrollRef.current,
    estimateSize: () => CELL_HEIGHT,
    overscan: OVERSCAN,
  });

  // This will replace "real" rows for rendering
  const virtualRows = virtualizer.getVirtualItems();

  const [before, after] =
    virtualRows.length > 0
      ? [
          notUndefined(virtualRows[0]).start - virtualizer.options.scrollMargin,
          virtualizer.getTotalSize() -
            notUndefined(virtualRows[virtualRows.length - 1]).end,
        ]
      : [0, 0];

  return { virtualRows, before, after };
};


Enter fullscreen mode Exit fullscreen mode

DataTable component adjustments

Now we change the DataTable component to use virtual rows. We are going to implement virtualization to efficiently render a huge number of rows in a table, improving performance by only rendering the rows that are visible in the viewport plus a few extra rows for smooth scrolling.

The virtualRows.map method iterates over each virtual row, rendering the corresponding “real” row from the rows array.

const DataTable: FC<Props> = ({ tableData }) => {
  //...

  /* Virtualizer logic start */
  const scrollRef = useRef<HTMLDivElement>(null);
  const { rows } = table.getRowModel();
  const { before, after, virtualRows } = useVirtualRows({
    scrollRef,
    rowsCount: rows.length,
  });
  /* Virtualizer logic end */

  return (
    //...
    <tbody>
      <Fragment>
        {before > 0 && (
          <tr>
            <td colSpan={columns.length} style={{height: before}} />
          </tr>
        )}
        {virtualRows.map((virtualRow) => {
          // this is the "real" current row
          const row = rows[virtualRow.index];
          return (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => /*...*/)}
            </tr>
          );
        })}
        {after > 0 && (
          <tr>
            <td colSpan={columns.length} style={{height: after}} />
          </tr>
        )}
      </Fragment>
    </tbody>
  );
};

Enter fullscreen mode Exit fullscreen mode

Working demo

So, now we can set rows' amount to 33333 and see what happens.

Next: Column pinning

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (0)

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay