Building High-Performance React Data Grids with TanStack Table v8

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

The Challenge of Large Datasets in React

Most frontend developers eventually hit a wall with data tables. When you are mapping over 50 rows, a simple .map() works perfectly. However, once your dataset grows to 1,000 or 10,000 records, the standard approach fails. The browser starts to stutter, interactions feel like they are lagging, and the DOM becomes too heavy to manage.

TanStack Table v8 is the go-to choice for solving these performance bottlenecks. It is a “headless” library. This means it manages the logic, state, and API for your table without forcing a specific UI on you. You keep total control over your HTML and CSS. In production environments, I have seen this approach keep interfaces snappy even when processing datasets exceeding 10,000 rows.

Step 1: Installation and Project Setup

First, grab the core table package. Since we want to handle massive amounts of data smoothly, we will also include the virtualization package from the same ecosystem.

npm install @tanstack/react-table @tanstack/react-virtual

If you prefer yarn or pnpm, use these commands instead:

yarn add @tanstack/react-table @tanstack/react-virtual
# or
pnpm add @tanstack/react-table @tanstack/react-virtual

Version 8 is built with TypeScript from the ground up. This provides excellent type safety, helping you catch potential bugs during development rather than discovering them after you deploy.

Step 2: Configuration and Core Logic

The core strength of TanStack Table v8 is its modularity. You only import the features you actually need, which keeps your bundle size small. Let’s set up a configuration that handles sorting and filtering.

Defining Columns and Data

Start by defining your data structure. We use the createColumnHelper to keep our cell renderers clean and type-safe.

import { createColumnHelper } from '@tanstack/react-table';

type User = {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  status: string;
};

const columnHelper = createColumnHelper<User>();

const columns = [
  columnHelper.accessor('id', {
    header: () => <span>ID</span>,
    cell: info => info.getValue(),
  }),
  columnHelper.accessor('firstName', {
    header: 'First Name',
  }),
  columnHelper.accessor('lastName', {
    header: 'Last Name',
  }),
  columnHelper.accessor('email', {
    header: 'Email',
  }),
  columnHelper.accessor('age', {
    header: 'Age',
  }),
];

Implementing the Table Hook

Think of the useReactTable hook as the brain of your component. You wire up your sorting and filtering models here. Because the library is modular, you must explicitly provide the row models you intend to use.

import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  SortingState,
} from '@tanstack/react-table';
import { useState } from 'react';

function DataTable({ data }) {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [globalFilter, setGlobalFilter] = useState('');

  const table = useReactTable({
    data,
    columns,
    state: {
      sorting,
      globalFilter,
    },
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
  });

  return (
    <div className="p-4">
      <input
        value={globalFilter ?? ''}
        onChange={e => setGlobalFilter(e.target.value)}
        placeholder="Search all columns..."
        className="mb-4 p-2 border rounded"
      />
      <table>
        {/* Headers and rows go here */}
      </table>
    </div>
  );
}

Boosting Speed with Virtualization

Rendering 5,000 rows at once creates 5,000 <tr> elements. A table with 15 columns would result in 75,000 DOM nodes, which can crash mobile browsers. Virtualization solves this by only rendering the rows currently visible in the viewport.

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

const tableContainerRef = useRef<HTMLDivElement>(null);
const { rows } = table.getRowModel();

const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => tableContainerRef.current,
  estimateSize: () => 35, // Average height of a row in pixels
  overscan: 10, // Pre-render 10 rows for smoother scrolling
});

const virtualRows = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();

In your JSX, apply an absolute position to the rows using the start value from the virtualizer. This tricks the browser into showing a large scroll area while the DOM stays lightweight.

Step 3: Verification and Monitoring

After setting up virtualization, you should confirm the performance gains are real. I typically follow a simple checklist to ensure the implementation is effective.

Check the DOM Node Count

Open Chrome DevTools and look at the Elements tab. Scroll through your list. If you have 10,000 items but only see roughly 30 <tr> elements in the DOM at once, your virtualization is working. This significantly lowers memory usage.

Monitor Component Re-renders

Use the React Profiler to record a session while filtering or sorting. If individual cells re-render unnecessarily, your performance will suffer. To fix this, define your column objects outside the component or wrap them in useMemo.

Avoid These Common Pitfalls

  • Missing CSS: Virtualization fails if the parent container lacks a fixed height and overflow: auto.
  • Unstable Columns: Defining columns inside the component body without memoization forces the table to reset on every render.
  • Heavy Logic: Keep your accessor functions fast. Move complex data transformations to the backend or process them before they reach the table state.

By following these steps, you build a data grid that feels responsive and professional. Users can scroll through massive datasets without the UI locking up. This setup also provides a clean foundation for adding features like column resizing or row grouping later on.

Share: