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.
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 };
};
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>
);
};
Working demo
So, now we can set rows' amount to 33333 and see what happens.
Next: Column pinning
Top comments (0)