Skip to main content

Best React Table Libraries in 2026

·PkgPulse Team
0

TL;DR

TanStack Table for custom, composable tables; AG Grid for enterprise data grids; react-data-grid for editable spreadsheet UIs. TanStack Table (~3M weekly downloads) is headless — zero UI, maximum flexibility, you bring the markup. AG Grid (~2M downloads) is the feature-complete enterprise grid with virtual scrolling, Excel export, column grouping, and 100+ built-in features. react-data-grid (~400K downloads) sits between them — fast, editable, and easier to configure than AG Grid. For most React apps, TanStack Table + shadcn/ui covers 90% of use cases at a fraction of the bundle cost.

Key Takeaways

  • TanStack Table: ~3M weekly downloads — headless, zero UI, composable, tree-shakes well
  • AG Grid Community: ~2M downloads — enterprise features free, virtual scroll, 1M+ rows
  • react-data-grid: ~400K downloads — Excel-like editing, virtual scroll, simpler API than AG Grid
  • TanStack Table + shadcn/ui — the most popular combination for new React projects in 2026
  • AG Grid Enterprise — Excel export, pivot tables, row grouping — paid (~$1K/dev/year)
  • @tanstack/react-virtual — complement to TanStack Table for large dataset virtualization

The React Table Landscape in 2026

React table libraries have converged on two distinct philosophies. The headless camp — led by TanStack Table — provides all the logic and state management with zero UI. You render whatever markup you want. The fully-featured camp — led by AG Grid — ships everything: layout, sorting, filtering, virtual scrolling, and dozens of enterprise features, all in one package.

Between these poles sits react-data-grid: it handles layout and rendering but stays simpler and lighter than AG Grid. It is the Excel-like spreadsheet option for teams that need inline editing without the full AG Grid surface area.

The right choice depends on whether you need a custom-designed table that matches your design system, an enterprise-grade data grid with built-in Excel export, or a fast editable grid with a practical default look.


Feature Comparison

LibraryWeekly DownloadsHeadlessVirtual ScrollExcel ExportInline EditingPricing
@tanstack/react-table~3MYesVia react-virtualNoManualFree
ag-grid-react (Community)~2MNoBuilt-inNoLimitedFree
ag-grid-react (Enterprise)~2MNoBuilt-inYesYes~$1K/dev/yr
react-data-grid~400KNoBuilt-inNoYesFree

TanStack Table v8: Headless and Composable

TanStack Table (~3M weekly downloads, package name @tanstack/react-table) is the successor to react-table. It is entirely headless — the library manages sorting, filtering, pagination, grouping, and column visibility state without rendering a single DOM element. You bring all the HTML and CSS.

This headless philosophy is TanStack Table's biggest strength and its only real learning curve. The payoff is complete freedom: your table can use Tailwind, shadcn/ui, CSS Modules, or any design system without any CSS conflicts or overrides. Bundle size is ~15KB gzipped, far below AG Grid's ~200KB.

// TanStack Table — define columns with full TypeScript types
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  flexRender,
  type ColumnDef,
  type SortingState,
} from '@tanstack/react-table';
import { useState } from 'react';

interface Package {
  name: string;
  version: string;
  downloads: number;
  size: number;
  license: string;
}

const columns: ColumnDef<Package>[] = [
  {
    accessorKey: 'name',
    header: 'Package',
    cell: ({ row }) => (
      <a href={`/packages/${row.original.name}`} className="font-mono text-blue-600">
        {row.original.name}
      </a>
    ),
  },
  {
    accessorKey: 'downloads',
    header: ({ column }) => (
      <button
        onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
        className="flex items-center gap-1 font-semibold"
      >
        Downloads
        {column.getIsSorted() === 'asc' ? ' ↑' : column.getIsSorted() === 'desc' ? ' ↓' : ' ↕'}
      </button>
    ),
    cell: ({ getValue }) => getValue<number>().toLocaleString(),
  },
  {
    accessorKey: 'size',
    header: 'Bundle Size',
    cell: ({ getValue }) => `${(getValue<number>() / 1024).toFixed(1)} KB`,
  },
  {
    accessorKey: 'license',
    header: 'License',
  },
];
// TanStack Table — full component with sorting, filtering, and pagination
function PackagesTable({ data }: { data: Package[] }) {
  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(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: { pagination: { pageSize: 25 } },
  });

  return (
    <div className="space-y-4">
      <input
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        placeholder="Search packages..."
        className="w-full max-w-sm rounded border px-3 py-2 text-sm"
      />

      <div className="rounded-md border">
        <table className="w-full text-sm">
          <thead className="bg-muted/50">
            {table.getHeaderGroups().map(headerGroup => (
              <tr key={headerGroup.id}>
                {headerGroup.headers.map(header => (
                  <th key={header.id} className="px-4 py-3 text-left font-medium">
                    {flexRender(header.column.columnDef.header, header.getContext())}
                  </th>
                ))}
              </tr>
            ))}
          </thead>
          <tbody>
            {table.getRowModel().rows.map(row => (
              <tr key={row.id} className="border-t hover:bg-muted/25">
                {row.getVisibleCells().map(cell => (
                  <td key={cell.id} className="px-4 py-3">
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <div className="flex items-center justify-between">
        <span className="text-sm text-muted-foreground">
          Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
        </span>
        <div className="flex gap-2">
          <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}
            className="rounded border px-3 py-1 text-sm disabled:opacity-50">
            Previous
          </button>
          <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}
            className="rounded border px-3 py-1 text-sm disabled:opacity-50">
            Next
          </button>
        </div>
      </div>
    </div>
  );
}

shadcn/ui Data Table: The Default Pattern in 2026

The most popular combination for new React projects in 2026 is TanStack Table with the shadcn/ui Data Table pattern. shadcn/ui's data table documentation provides a complete, production-ready implementation — column definitions, sorting, filtering, pagination, and column visibility — using TanStack Table for logic and shadcn/ui's Table, Button, Input, and DropdownMenu components for the UI.

// shadcn/ui + TanStack Table — column visibility toggle (standard pattern)
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import type { Table } from "@tanstack/react-table";

function ColumnVisibilityToggle({ table }: { table: Table<Package> }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="sm">
          Columns
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        {table
          .getAllColumns()
          .filter(column => column.getCanHide())
          .map(column => (
            <DropdownMenuCheckboxItem
              key={column.id}
              checked={column.getIsVisible()}
              onCheckedChange={(value) => column.toggleVisibility(value)}
            >
              {column.id}
            </DropdownMenuCheckboxItem>
          ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

This pattern gives you full control over the UI with zero fighting against library CSS, because TanStack Table ships zero styles. The shadcn/ui components provide consistent design system integration, and column visibility toggles, row selection, and bulk actions all work naturally within the same component patterns you already use in the rest of the application.


AG Grid Community: Enterprise Features for Free

AG Grid (~2M weekly downloads) is the most feature-complete React data grid available. The Community edition is free and includes virtual scrolling that handles millions of rows, column resizing and reordering, built-in filtering, row selection, pinned columns, and row grouping.

The Enterprise edition adds Excel export, pivot tables, master-detail rows, tree data, charts integration, and server-side row model with infinite scrolling.

// AG Grid Community — enterprise data grid setup
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-quartz.css';
import type { ColDef, GridReadyEvent } from 'ag-grid-community';
import { useCallback, useRef } from 'react';

const columnDefs: ColDef<Package>[] = [
  {
    field: 'name',
    headerName: 'Package',
    filter: true,
    sortable: true,
    pinned: 'left',
  },
  {
    field: 'downloads',
    headerName: 'Weekly Downloads',
    sortable: true,
    filter: 'agNumberColumnFilter',
    valueFormatter: ({ value }) => value?.toLocaleString() ?? '0',
  },
  {
    field: 'size',
    headerName: 'Bundle Size',
    sortable: true,
    valueFormatter: ({ value }) => `${(value / 1024).toFixed(1)} KB`,
  },
  {
    headerName: 'Actions',
    cellRenderer: ({ data }: { data: Package }) => (
      <button onClick={() => window.open(`/compare/${data.name}`)}>Compare</button>
    ),
    sortable: false,
    filter: false,
    width: 100,
  },
];

function PackageGrid({ rowData }: { rowData: Package[] }) {
  const gridRef = useRef<AgGridReact>(null);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    params.api.sizeColumnsToFit();
  }, []);

  const exportToExcel = useCallback(() => {
    gridRef.current?.api.exportDataAsExcel();  // Enterprise feature only
  }, []);

  return (
    <div className="ag-theme-quartz" style={{ height: 600 }}>
      <AgGridReact
        ref={gridRef}
        rowData={rowData}
        columnDefs={columnDefs}
        pagination={true}
        paginationPageSize={50}
        rowSelection="multiple"
        onGridReady={onGridReady}
        animateRows={true}
        // Virtual scrolling handles 1M+ rowsbuilt-in, no configuration needed
      />
    </div>
  );
}

AG Grid's virtual scrolling is genuinely impressive for large datasets. It renders only the visible rows in the DOM and recycles them as you scroll, making it smooth even with one million rows of client-side data. This is a fundamentally different capability from TanStack Table's client-side row models, which require explicit integration with @tanstack/react-virtual to achieve the same effect.


react-data-grid: Spreadsheet-Like Editing

react-data-grid (~400K weekly downloads) fills a specific niche: Excel-like inline editing with built-in virtual scrolling and a simpler configuration API than AG Grid. Users can click directly on cells to edit them in place, making it ideal for data entry and admin interfaces.

// react-data-grid — Excel-like editable grid
import DataGrid, { textEditor, type Column } from 'react-data-grid';
import { useState } from 'react';

interface Row {
  id: number;
  name: string;
  downloads: number;
  license: string;
}

const columns: Column<Row>[] = [
  {
    key: 'id',
    name: 'ID',
    width: 60,
    frozen: true,   // Pin column to left (equivalent of AG Grid's pinned: 'left')
  },
  {
    key: 'name',
    name: 'Package',
    editor: textEditor,  // Built-in text editor — click to edit inline
    editorOptions: { editOnClick: true },
  },
  {
    key: 'downloads',
    name: 'Downloads',
    renderCell: ({ row }) => row.downloads.toLocaleString(),
  },
  {
    key: 'license',
    name: 'License',
    editor: textEditor,
  },
];

function EditablePackageGrid() {
  const [rows, setRows] = useState<Row[]>([
    { id: 1, name: 'react', downloads: 25000000, license: 'MIT' },
    { id: 2, name: 'next', downloads: 9000000, license: 'MIT' },
  ]);

  return (
    <DataGrid
      columns={columns}
      rows={rows}
      onRowsChange={setRows}    // Inline edits flow through this callback
      rowKeyGetter={(row) => row.id}
      style={{ height: '100%' }}
      // Virtual scrolling is built-in — no additional configuration needed
    />
  );
}

The onRowsChange callback is the simplest API for inline editing among the three libraries. Changes propagate through the callback without any custom cell renderer boilerplate. The trade-off is customization depth: react-data-grid's styling and theming are less flexible than TanStack Table, and it lacks AG Grid's feature breadth.


TanStack Virtual: Virtualization for Large Datasets

For large datasets with TanStack Table, adding @tanstack/react-virtual enables row virtualization comparable to AG Grid's built-in virtual scrolling.

// TanStack Table + @tanstack/react-virtual — row virtualization
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

function VirtualizedTable({ data }: { data: Package[] }) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

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

  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40,   // Estimated row height in pixels
    overscan: 20,             // Render extra rows outside viewport for smooth scrolling
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <table style={{ width: '100%' }}>
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th key={header.id} className="px-4 py-2 text-left sticky top-0 bg-white">
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
          {virtualizer.getVirtualItems().map(virtualRow => {
            const row = rows[virtualRow.index];
            return (
              <tr
                key={row.id}
                style={{
                  position: 'absolute',
                  top: 0,
                  transform: `translateY(${virtualRow.start}px)`,
                  width: '100%',
                }}
              >
                {row.getVisibleCells().map(cell => (
                  <td key={cell.id} className="px-4 py-2">
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

With @tanstack/react-virtual, TanStack Table can handle datasets comparable in size to AG Grid Community — hundreds of thousands of rows. The main difference is implementation cost: AG Grid's virtualization is automatic, whereas the TanStack approach requires explicit wiring. For teams that value the headless flexibility, this is an acceptable trade-off.


Server-Side Pagination

For very large datasets stored in a database, client-side rendering is not viable regardless of the library. All three support server-side data with different APIs.

// TanStack Table — server-side pagination with manualPagination
function ServerSideTable() {
  const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
  const [sorting, setSorting] = useState<SortingState>([]);

  const { data, isLoading } = useQuery({
    queryKey: ['packages', pagination, sorting],
    queryFn: () => fetch(
      `/api/packages?page=${pagination.pageIndex}&limit=${pagination.pageSize}` +
      (sorting.length ? `&sort=${sorting[0].id}&order=${sorting[0].desc ? 'desc' : 'asc'}` : '')
    ).then(r => r.json()),
  });

  const table = useReactTable({
    data: data?.rows ?? [],
    columns,
    pageCount: data?.pageCount ?? -1,
    state: { pagination, sorting },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
    getCoreRowModel: getCoreRowModel(),
    manualPagination: true,    // Disable client-side pagination
    manualSorting: true,       // Disable client-side sorting
  });

  if (isLoading) return <TableSkeleton />;

  return <StandardTableLayout table={table} />;
}

AG Grid's server-side equivalent uses the datasource API with the infinite row model, which provides AG Grid-managed scroll position and loading states. TanStack Table's manualPagination: true approach gives you full control over the API call shape, which makes it more flexible for non-standard or GraphQL APIs. The difference is largely a matter of which abstraction layer you prefer.


Package Health

PackageWeekly DownloadsBundle SizeTypeScriptLicense
@tanstack/react-table~3M~15KB gzippedNativeMIT
ag-grid-react (Community)~2M~200KB gzippedNativeMIT
react-data-grid~400K~80KB gzippedNativeMIT

When to Choose

Choose TanStack Table when:

  • You need a fully custom UI that matches your design system (shadcn/ui, Radix, Material)
  • Your dataset is small to medium in size (under 50K rows without virtualization)
  • You want the smallest possible bundle contribution from the table library
  • Building complex tables with custom cell renderers, row actions, or nested components

Choose TanStack Table + @tanstack/react-virtual when:

  • The above applies but you need to handle 100K+ rows efficiently
  • You want TanStack Table's flexibility plus AG Grid-level scroll performance
  • You are comfortable writing the virtualization integration manually

Choose AG Grid Community when:

  • You need built-in virtual scrolling for very large datasets without extra setup
  • Column resizing, reordering, pinning, and row grouping are required out of the box
  • The larger bundle size (~200KB) is acceptable for the feature trade-off
  • Building a data-heavy internal tool quickly without custom table layout work

Choose AG Grid Enterprise when:

  • Excel export is a business requirement for your users
  • You need pivot tables, master-detail views, or server-side infinite scrolling
  • Budget allows for enterprise licensing and the feature set justifies it
  • Building an internal data platform, analytics dashboard, or financial tool

Choose react-data-grid when:

  • Inline cell editing is the primary requirement
  • You want an Excel-like spreadsheet interface with a simpler setup than AG Grid
  • Datasets are medium-sized (10K–100K rows) with built-in virtual scrolling
  • The team wants a practical default UI without full custom table layout work

When Virtualization Is Necessary vs Overkill

Virtual scrolling is not free. The TanStack Virtual approach uses absolute positioning and transform offsets to place rows in the viewport — this breaks the natural document flow and requires a fixed-height scroll container. Sticky headers, dynamic row heights, and certain CSS layout interactions become more complex when virtualization is active. The overhead is worth accepting when the dataset is large enough that rendering all rows would cause the DOM to balloon to thousands of nodes, degrading browser rendering performance and memory usage.

The practical threshold where virtualization becomes necessary is roughly 1,000 to 5,000 rows, depending on column count and cell renderer complexity. Below 1,000 rows with simple cell renderers, standard DOM rendering is fast enough that virtualization adds complexity without benefit. Above 5,000 rows, the DOM node count starts affecting scroll performance even on modern hardware, and users begin to notice. For any dataset where you are genuinely unsure whether the user will scroll through thousands of rows, it is safer to implement virtualization from the start rather than retrofit it — retrofitting requires restructuring the table's layout code, not just adding a hook.

AG Grid's virtualization automatically handles variable row heights through a height measurement pass during initial render, then caches heights for scroll position calculation. TanStack Virtual's estimateSize function is used for initial layout and position calculation, but it assumes uniform row height unless you implement dynamic measurement with measureElement. For tables where cell content varies significantly in height (long text, expandable rows, image thumbnails), AG Grid's automatic height measurement is a meaningful advantage over the manual TanStack Virtual approach.


Server-Side Operations: When Client-Side Falls Short

Client-side sorting, filtering, and pagination work well until the dataset size exceeds what you can reasonably load into the browser. The inflection point is roughly 10,000-50,000 rows — beyond that, loading all records on page load creates unacceptable initial load times and memory pressure. The transition to server-side operations is not an optimization; it is a correctness requirement for any table that will be used with real production data at scale.

TanStack Table's manualPagination, manualSorting, and manualFiltering flags disable the respective client-side logic and give you full control over the API call shape. The state still lives in TanStack Table (pagination index, sort column, filter values), but the data fetching is your responsibility. This is the right model for GraphQL APIs, APIs that use cursor-based pagination instead of offset-based, or backends with non-standard filter parameter formats — TanStack Table does not impose an API contract on the server side.

The pattern for combining TanStack Table with TanStack Query for server-side tables is well-established: the table state drives the queryKey, the query fetches from the server, and the response populates both the data and the pageCount (or total row count for offset pagination). This approach gives you cache invalidation, background refetching, and optimistic updates for free from TanStack Query, layered on top of TanStack Table's state management. AG Grid's equivalent server-side row model is more opinionated — it works best when the server can be structured to match AG Grid's expected request/response format, which is a reasonable choice when you control the API and want the grid to own the data fetching logic entirely.


Related: Best React Animation Libraries 2026, TanStack Table package health, Best Form Libraries for React 2026

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.