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 scrolling — handles millions of rows
rowModelType="clientSide"
animateRows={true}
/>
</div>
);
}
Comparison Table
| Feature | TanStack Table | AG Grid Community | AG Grid Enterprise |
|---|---|---|---|
| Bundle Size | ~15KB | ~200KB | ~200KB |
| Virtual Scroll | Manual | ✅ Built-in | ✅ Built-in |
| Excel Export | ❌ | ❌ | ✅ |
| Pivot Tables | ❌ | ❌ | ✅ |
| Row Grouping | Manual | ✅ | ✅ |
| Price | Free | Free | ~$1K/dev/year |
| Headless | ✅ | ❌ | ❌ |
When to Choose
| Scenario | Pick |
|---|---|
| Custom design, shadcn/ui style | TanStack Table |
| Simple data table with sort/filter/pagination | TanStack Table + shadcn |
| 100K+ rows, virtual scrolling | AG Grid Community |
| Excel export, pivot, column grouping | AG Grid Enterprise |
| Editable spreadsheet-like grid | react-data-grid |
| Server-side data (pagination via API) | TanStack Table (manual) or AG Grid |
Compare table library package health on PkgPulse.
See the live comparison
View tanstack table vs. ag grid on PkgPulse →