Skip to main content

@tanstack/react-table vs AG Grid vs react-data-grid: Data Tables in 2026

·PkgPulse Team

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

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 →

Comments

Stay Updated

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