Skip to main content

Best React Table Libraries in 2026

·PkgPulse Team

TL;DR

TanStack Table for custom, composable tables; AG Grid for enterprise data grids. 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. For most React apps, TanStack Table + shadcn/ui data table 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: ~2M downloads — enterprise features, virtual scroll, Excel, pivot, row grouping
  • react-data-grid: ~400K downloads — fast, Excel-like editing, good middle ground
  • TanStack Table + shadcn — the most popular combination for new React projects in 2026
  • AG Grid Community — free tier (powerful), AG Grid Enterprise — paid ($1K+/dev/year)

TanStack Table (Headless)

// 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">
        {row.original.name}
      </a>
    ),
  },
  {
    accessorKey: 'downloads',
    header: ({ column }) => (
      <button
        onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
        className="flex items-center gap-1"
      >
        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 table component
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>
      <input
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        placeholder="Filter packages..."
        className="mb-4 p-2 border rounded"
      />

      <table className="w-full border-collapse">
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th key={header.id} className="border p-2 text-left bg-gray-100">
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map(row => (
            <tr key={row.id} className="hover:bg-gray-50">
              {row.getVisibleCells().map(cell => (
                <td key={cell.id} className="border p-2">
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>

      {/* Pagination */}
      <div className="flex items-center gap-2 mt-4">
        <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
          Previous
        </button>
        <span>
          Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
        </span>
        <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
          Next
        </button>
      </div>
    </div>
  );
}

AG Grid (Enterprise)

// AG Grid — enterprise data grid
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',  // Pin column
  },
  {
    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
  }, []);

  return (
    <div className="ag-theme-quartz" style={{ height: 600 }}>
      <button onClick={exportToExcel}>Export to Excel</button>
      <AgGridReact
        ref={gridRef}
        rowData={rowData}
        columnDefs={columnDefs}
        pagination={true}
        paginationPageSize={50}
        rowSelection="multiple"
        onGridReady={onGridReady}
        // Virtual scrollinghandles millions of rows
        rowModelType="clientSide"
        animateRows={true}
      />
    </div>
  );
}

Comparison Table

FeatureTanStack TableAG Grid CommunityAG Grid Enterprise
Bundle Size~15KB~200KB~200KB
Virtual ScrollManual✅ Built-in✅ Built-in
Excel Export
Pivot Tables
Row GroupingManual
PriceFreeFree~$1K/dev/year
Headless

When to Choose

ScenarioPick
Custom design, shadcn/ui styleTanStack Table
Simple data table with sort/filter/paginationTanStack Table + shadcn
100K+ rows, virtual scrollingAG Grid Community
Excel export, pivot, column groupingAG Grid Enterprise
Editable spreadsheet-like gridreact-data-grid
Server-side data (pagination via API)TanStack Table (manual) or AG Grid

Compare table library package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.