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

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.