Skip to main content

Guide

TanStack Table vs AG Grid vs react-data-grid 2026

TanStack Table, AG Grid, and react-data-grid compared for React tables in 2026. Headless vs opinionated, virtual scrolling, inline editing, and bundle size.

·PkgPulse Team·
0

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

PackageWeekly DownloadsLicenseBundle Size
@tanstack/react-table~1.6MMIT~45KB
ag-grid-community~1.2MMIT (Community)~320KB
ag-grid-react~1.1MMIT~10KB (wrapper)
react-data-grid~500KMIT~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-tableAG Grid Communityreact-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).

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.