TL;DR
@tanstack/react-table is the right choice for most React applications — headless, composable, and framework-agnostic, it lets you build exactly the table you need with no UI opinions. AG Grid Community is the right choice when you need Excel-like features (inline editing, copy-paste, pivot tables) and are willing to accept its size. react-data-grid hits the sweet spot for spreadsheet-style tables with a smaller footprint than AG Grid.
Key Takeaways
- @tanstack/react-table: ~1.6M weekly downloads — headless table logic, you supply the UI
- AG Grid Community: ~1.2M weekly downloads — most feature-complete, enterprise-grade, larger bundle
- react-data-grid: ~500K weekly downloads — spreadsheet-style cells, inline editing, virtual rows
- All three support virtualization for large datasets (100K+ rows)
- TanStack Table wins for custom UI and integration with the TanStack ecosystem
- AG Grid wins for Excel-like functionality (pivot, aggregation, inline editing at scale)
- react-data-grid wins for spreadsheet-style interfaces at moderate complexity
Download Trends
| Package | Weekly Downloads | License | Bundle Size |
|---|---|---|---|
@tanstack/react-table | ~1.6M | MIT | ~45KB |
ag-grid-community | ~1.2M | MIT (Community) | ~320KB |
ag-grid-react | ~1.1M | MIT | ~10KB (wrapper) |
react-data-grid | ~500K | MIT | ~90KB |
@tanstack/react-table
TanStack Table (v8) is a headless table utility — it provides all the logic for sorting, filtering, pagination, grouping, and column resizing, but zero markup or styling:
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
useReactTable,
type SortingState,
} from "@tanstack/react-table"
import { useState } from "react"
type Package = {
name: string
version: string
downloads: number
size: number
license: string
}
const columnHelper = createColumnHelper<Package>()
const columns = [
columnHelper.accessor("name", {
header: "Package",
cell: (info) => (
<a href={`/packages/${info.getValue()}`} className="font-mono text-blue-500">
{info.getValue()}
</a>
),
}),
columnHelper.accessor("downloads", {
header: "Weekly Downloads",
cell: (info) => info.getValue().toLocaleString(),
sortingFn: "basic",
}),
columnHelper.accessor("size", {
header: "Bundle Size",
cell: (info) => `${(info.getValue() / 1024).toFixed(1)}KB`,
}),
columnHelper.accessor("license", {
header: "License",
filterFn: "equals",
}),
]
function PackageTable({ data }: { data: Package[] }) {
const [sorting, setSorting] = useState<SortingState>([])
const [globalFilter, setGlobalFilter] = useState("")
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: { sorting, globalFilter },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
initialState: { pagination: { pageSize: 25 } },
})
return (
<div>
<input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search packages..."
className="border rounded px-3 py-2 mb-4"
/>
<table className="w-full border-collapse">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="text-left p-2 border-b bg-gray-50 cursor-pointer select-none"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: " ↑", desc: " ↓" }[header.column.getIsSorted() as string] ?? ""}
</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="p-2 border-b">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
{/* Pagination: */}
<div className="flex 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>
)
}
TanStack Table + Virtual rows (100K rows):
import { useVirtualizer } from "@tanstack/react-virtual"
function VirtualTable({ data }: { data: Package[] }) {
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
const { rows } = table.getRowModel()
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
overscan: 20,
})
return (
<div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
<table>
<thead>{/* Headers */}</thead>
<tbody style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<tr
key={row.id}
ref={virtualizer.measureElement}
data-index={virtualRow.index}
style={{ position: "absolute", transform: `translateY(${virtualRow.start}px)` }}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
AG Grid
AG Grid Community is the most feature-complete data grid available in React — it's the closest thing to Microsoft Excel in a browser:
import { AgGridReact } from "ag-grid-react"
import { ColDef, GridReadyEvent, CellValueChangedEvent } from "ag-grid-community"
import "ag-grid-community/styles/ag-grid.css"
import "ag-grid-community/styles/ag-theme-quartz.css"
const columnDefs: ColDef[] = [
{ field: "name", headerName: "Package", filter: true, sortable: true, pinned: "left" },
{
field: "downloads",
headerName: "Weekly Downloads",
sortable: true,
filter: "agNumberColumnFilter",
valueFormatter: (p) => p.value.toLocaleString(),
cellStyle: { textAlign: "right" },
},
{
field: "size",
headerName: "Bundle Size",
editable: true, // Inline editing!
valueFormatter: (p) => `${(p.value / 1024).toFixed(1)}KB`,
},
{ field: "license", filter: true, floatingFilter: true },
]
function AGGridTable({ rowData }: { rowData: Package[] }) {
const onCellValueChanged = (event: CellValueChangedEvent) => {
console.log(`${event.colDef.field} changed: ${event.oldValue} → ${event.newValue}`)
// Persist the edit
}
return (
<div className="ag-theme-quartz" style={{ height: 500, width: "100%" }}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={{ resizable: true, sortable: true }}
pagination={true}
paginationPageSize={50}
onCellValueChanged={onCellValueChanged}
// Virtualization built-in:
rowVirtualizationThreshold={500}
// Row selection:
rowSelection="multiple"
// Excel-style copy/paste:
enableCellTextSelection={true}
// Column groups:
suppressColumnVirtualisation={false}
/>
</div>
)
}
AG Grid Enterprise features (paid license):
- Pivot tables and row grouping
- Server-side row model (infinite scroll from API)
- Clipboard integration (Excel copy-paste)
- Master/detail rows
- Set filters
- Integrated charts
AG Grid Community limitations vs Enterprise:
// Community is free and includes:
// - Sorting, filtering, pagination
// - Inline editing (basic)
// - Row drag-and-drop
// - Themes
// Enterprise adds ($$$ license):
// - groupRowsByColumn (grouping + aggregation)
// - serverSideRowModel
// - setFilter (multi-select filter)
// - masterDetail
// - clipboard (Ctrl+C/V like Excel)
// - rangeSelection (Excel-style cell range selection)
react-data-grid
react-data-grid (by Adazzle) is a spreadsheet-focused grid optimized for inline editing:
import DataGrid, { Column, textEditor, SelectCellFormatter } from "react-data-grid"
import "react-data-grid/lib/styles.css"
type Row = { id: number; package: string; version: string; downloads: number }
const columns: Column<Row>[] = [
{ key: "id", name: "ID", width: 60, frozen: true },
{
key: "package",
name: "Package",
editor: textEditor, // Inline text editing
frozen: true,
},
{ key: "version", name: "Version", editor: textEditor },
{
key: "downloads",
name: "Downloads",
renderCell: ({ row }) => row.downloads.toLocaleString(),
},
]
function SpreadsheetGrid() {
const [rows, setRows] = useState<Row[]>(data)
return (
<DataGrid
columns={columns}
rows={rows}
onRowsChange={setRows} // Called when cells are edited
rowKeyGetter={(row) => row.id}
// Virtual scrolling built-in — handles 100K+ rows:
className="rdg-light"
style={{ height: 500 }}
/>
)
}
react-data-grid is smaller than AG Grid and specifically optimized for spreadsheet-style interfaces. The inline editing model (onRowsChange) is elegant — you receive the updated rows array and decide how to persist.
Feature Comparison
| Feature | @tanstack/react-table | AG Grid Community | react-data-grid |
|---|---|---|---|
| Headless | ✅ | ❌ | ❌ |
| Bundle size | ~45KB | ~320KB | ~90KB |
| Built-in UI | ❌ | ✅ | ✅ |
| Virtual scrolling | ✅ (via @tanstack/virtual) | ✅ Built-in | ✅ Built-in |
| Inline cell editing | ❌ DIY | ✅ | ✅ Built-in |
| Column pinning | ✅ | ✅ | ✅ |
| Column resizing | ✅ | ✅ | ✅ |
| Row grouping | ✅ (logic only) | ✅ | ❌ |
| Pivot tables | ❌ | ✅ Enterprise | ❌ |
| Excel copy-paste | ❌ | ✅ Enterprise | ✅ Community |
| Tree data | ✅ | ✅ | ❌ |
| Server-side pagination | ✅ | ✅ Enterprise | ❌ |
| TypeScript | ✅ | ✅ | ✅ |
| Framework agnostic | ✅ | ❌ React/Angular/Vue | ❌ React-only |
When to Use Each
Choose @tanstack/react-table if:
- You're using shadcn/ui or Tailwind (headless = full design control)
- You want framework-agnostic table logic (Vue, Solid, Angular available)
- The table is part of a larger product UI — not a standalone data tool
- You're already using TanStack Query or Router
Choose AG Grid Community if:
- Users need Excel-like experience (keyboard navigation, inline editing, clipboard)
- You have 100K+ rows and need industrial-strength virtualization
- Column filtering, floating filters, and aggregation are requirements
- Enterprise features aren't needed (or you'll pay for the license)
Choose react-data-grid if:
- You need spreadsheet-style cell editing without the AG Grid bundle size
- Excel copy-paste (Ctrl+C/V) is required without Enterprise license
- Clean row-based inline editing model (
onRowsChange) fits your data flow
Migration Guide
From react-table v7 to @tanstack/react-table v8
The TanStack Table v8 rewrite was a breaking change from react-table v7. The core concept is the same (headless logic) but the API changed significantly:
// react-table v7 (old)
import { useTable, useSortBy, useFilters, usePagination } from "react-table"
const columns = useMemo(() => [
{ Header: "Package", accessor: "name" },
{ Header: "Downloads", accessor: "downloads" },
], [])
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(
{ columns, data },
useFilters,
useSortBy,
usePagination
)
// @tanstack/react-table v8 (new)
import { createColumnHelper, useReactTable, getCoreRowModel, getSortedRowModel } from "@tanstack/react-table"
const columnHelper = createColumnHelper<Package>()
const columns = [
columnHelper.accessor("name", { header: "Package" }),
columnHelper.accessor("downloads", { header: "Downloads" }),
]
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
The key difference: v8 uses explicit model factories (getCoreRowModel, getSortedRowModel) imported separately, rather than hooks passed to useTable. This enables better tree-shaking.
From react-table/TanStack Table to AG Grid (adding editing)
When your application has evolved from a read-only data table to one requiring inline editing, pivot tables, or Excel-like interactions, migrating to AG Grid makes sense:
// Before: TanStack Table with custom editing (complex)
function EditableCell({ getValue, row, column, table }) {
const [editing, setEditing] = useState(false)
const [value, setValue] = useState(getValue())
if (editing) {
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => {
table.options.meta?.updateData(row.index, column.id, value)
setEditing(false)
}}
autoFocus
/>
)
}
return <span onDoubleClick={() => setEditing(true)}>{value}</span>
}
// After: AG Grid with built-in editing (simple)
const columnDefs = [
{ field: "name", editable: true }, // Just set editable: true
{ field: "downloads", editable: true }, // AG Grid handles the rest
]
// Plus onCellValueChanged callback for persistence
Community Adoption in 2026
@tanstack/react-table reaches approximately 1.6 million weekly downloads, representing the de facto standard for custom data table implementations in the React ecosystem. Its growth has been driven by the broader TanStack ecosystem adoption: teams using TanStack Query (React Query) for server state naturally adopt TanStack Table for data display, and TanStack Router integration is increasingly common in larger applications. The shadcn/ui Data Table example — built on TanStack Table with a Tailwind-styled table component — has become the reference implementation for React data tables in design systems and enterprise apps. Because it's headless, every TanStack Table implementation looks different: some teams ship plain HTML tables with Tailwind, others use styled components, and others integrate with component libraries like Radix or Chakra.
AG Grid Community at approximately 1.2 million weekly downloads represents the enterprise data grid market. AG Grid's total downloads (community + enterprise wrappers) are higher, but the split reflects the pricing model: teams often start with Community and upgrade to Enterprise when they need pivot tables, clipboard integration, or server-side row models. AG Grid is the default choice for fintech dashboards, data analytics tools, and internal admin panels where users expect Excel-like behavior. The ag-theme-quartz design refresh (replacing the older Material and Balham themes) significantly improved the default aesthetics, reducing the need for custom CSS to achieve a professional appearance.
react-data-grid at approximately 500,000 weekly downloads serves a specific niche between TanStack Table's headlessness and AG Grid's comprehensiveness. Its built-in copy-paste functionality (Ctrl+C/V selects cell ranges and pastes as tab-separated values, compatible with Excel) is available in the Community (MIT) version — unlike AG Grid, which gates this behind the Enterprise license. For teams building internal tools with spreadsheet-like data entry requirements who cannot justify AG Grid's Enterprise cost, react-data-grid is the pragmatic choice.
Performance at Scale: Virtualization Strategy
All three libraries handle large datasets through row virtualization — rendering only the rows visible in the viewport rather than the full dataset. The implementations differ in how they handle this, with meaningful implications for smoothness and correctness.
TanStack Table provides the logic model but not the virtualization itself. You add @tanstack/react-virtual separately to render virtualized rows. This separation of concerns means you have full control over the DOM structure: the virtualizer computes which rows to render and their positions, and your render function outputs exactly the markup you define. The flexibility is real — you can use CSS Grid, absolute positioning, or any layout strategy — but setting it up correctly for the first time requires understanding both TanStack Table's row model and TanStack Virtual's measureElement / estimateSize API. For 100K+ row tables, the result is highly performant because you're not fighting against any opinionated DOM structure.
AG Grid's virtualization is built-in and requires no additional configuration. The grid renders a fixed-height container and manages row recycling internally. The rowVirtualizationThreshold option controls when virtualization activates (default 500 rows). AG Grid's virtualization is battle-tested against very large datasets — internal fintech benchmarks with 500K rows and real-time updates are a documented use case for AG Grid Enterprise's server-side row model. The tradeoff is opacity: when virtualization behaves unexpectedly (rows jumping, incorrect heights for variable-height content), debugging requires understanding AG Grid's internal row height management.
react-data-grid virtualizes rows and columns simultaneously — it uses absolute-positioned rows and fixed column widths to keep the render tree shallow regardless of data size. The column virtualization is a genuine advantage for wide spreadsheets (50+ columns), where neither TanStack Table's DOM-based columns nor AG Grid Community's column virtualization (Enterprise-gated) performs as well. For the spreadsheet use case where users expect Excel-like behavior — scrolling through hundreds of columns — react-data-grid's default configuration handles this correctly without additional setup.
Methodology
Download data from npm registry (weekly average, February 2026). Bundle sizes from bundlephobia. Feature comparison based on official documentation for @tanstack/react-table v8.x, ag-grid-community v31.x, and react-data-grid v7.x.
Compare data table packages on PkgPulse →
See also: React vs Vue and React vs Svelte, Best React Form Libraries (2026).