Skip to content

Frontend

TanStack Table vs AG Grid — Data Tables Aidxn Reaches For

Data tables are the workhorse of SaaS dashboards. AG Grid ships 150+ features, paid licensing, and 500KB gzip. TanStack Table ships headless state management — you bring the UI. Aidxn rule: TanStack Table for filtering, sorting, virtual scroll, and custom builds. AG Grid Community only when you need pivot tables, master-detail, or Excel parity. Here's why headless tables won the 2026 data table wars.

📊 ⚙️

Two years ago you built a staff roster. Data lives in Supabase, you threw Material-UI's DataGrid at it, added filtering and sorting, shipped it. Eighteen months later the designer wants the table rows to be checkboxes that feed a bulk-action toolbar. The pagination footers need custom styling. Mobile needs horizontal scroll with sticky left columns. You're three hours into MUI DataGrid prop-drilling, reading a 40-line table config, and realizing you're fighting the component philosophy. Your colleague ships a TanStack Table table in an afternoon — same data, custom markup, filtering and sorting built-in, infinite scroll working. You ask why nobody told you.

This is the data table decision tree most SaaS teams get wrong. AG Grid is legendary in enterprise finance (banks, trading floors, Bloomberg terminals). It owns pivot tables, real-time cell updates, server-side aggregation, Excel copy-paste. But enterprise features come with enterprise licensing and 500KB bundle hits. Material-UI DataGrid, AG Grid Community, Mantine Table — all component-first. You get a table component and drill props. TanStack Table landed in 2020 as "headless table" — state management without UI. You own the markup, TanStack Table owns filtering, sorting, pagination, grouping, virtual scrolling. Separation of concerns, builder's freedom, bundle size under 20KB. Once you taste it, every other table library feels handcuffed.

Three Philosophies, Three Problems They Solve

AG Grid: Enterprise grade, ships everything. You import AgGridReact, mount a component, pass config, get a fully-featured table. Cell editors, column resizing, server-side filtering, grouping by 5 columns, export to Excel, copy-paste, keyboard shortcuts. Bundle: 500KB gzip for Community, more for Enterprise. License: Community is free (with attribution), Pro/Enterprise are paid. Philosophy: "We built the table so you don't have to." Best if: your data is complex (finance, real estate, logistics) or you need Excel parity or master-detail drilling with server-side aggregation. Worst if: you have a simple SaaS dashboard, a mobile app, or designers who own the component markup.

Material-UI / Mantine DataGrid: Component-first, unopinionated styling. DataGrid is a table component with theming props, column definitions, and event handlers. You customize via props (sx in MUI, className chains in Mantine). Bundle: 150KB+ (includes the UI framework). Philosophy: "We built a base table, theme it yourself." Best if: you're already on Material-UI or Mantine and want consistency. Worst if: your designer has specific row styling or you're building a custom vertical-scroll infinite-load table — you're fighting the component mental model.

TanStack Table (React Table): Headless state management. You import useReactTable, pass columns and data, get back sorting, filtering, pagination, row selection, virtual scrolling. No UI — you write the JSX (<table>, <thead>, <tr>, etc.). Bundle: 18KB gzip. License: MIT. Philosophy: "We handle the logic, you own the markup." Best if: you need fine-grained control, custom styling, mobile-responsive tables, or multi-framework support (TanStack Table works in Vue, Svelte, Solid). Worst if: you want a drag-drop table builder or need master-detail drilling (you'll write it). This is what Velocity X ships for staff rosters, location sorting, analytics dashboards.

The Comparison Table

Dimension AG Grid MUI DataGrid TanStack Table
Bundle 500KB gzip 150KB gzip 18KB gzip
Learning Curve Steep (250+ props) Moderate (component-first) Low (hooks + JSX)
Styling Freedom CSS classes (theme) sx prop / className 100% (you own markup)
Virtual Scroll Native Via plugin Native (@tanstack/react-virtual)
Filtering Server-side or client Client-side Agnostic (you choose)
License / Cost Community free / Pro paid Free (MIT) Free (MIT)
Pivot / Master-Detail Built-in No You build it
Best For Enterprise, Excel parity MUI ecosystem SaaS dashboards, custom

Minimal TanStack Table Setup

Here's a filterable, sortable staff roster with sorting UI and virtual scroll. No component props, pure JSX control:

import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';

const columns = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'role', header: 'Role' },
  { accessorKey: 'status', header: 'Status' },
];

export function StaffRoster({ data }) {
  const [sorting, setSorting] = useState([]);
  const [globalFilter, setGlobalFilter] = useState('');

  const table = useReactTable({
    data,
    columns,
    state: { sorting, globalFilter },
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
  });

  const { rows } = table.getRowModel();

  return (
    <div className="space-y-4">
      <input
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        placeholder="Search staff..."
        className="w-full px-3 py-2 border rounded"
      />
      <table className="w-full border-collapse border border-slate-300">
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id} className="bg-slate-50">
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  className="px-4 py-2 text-left cursor-pointer hover:bg-slate-100"
                  onClick={() => header.column.toggleSorting()}
                >
                  {header.isPlaceholder
                    ? null
                    : (
                      <>
                        {flexRender(header.column.columnDef.header, header.getContext())}
                        {header.column.getIsSorted() && (
                          <span className="ml-2">
                            {header.column.getIsSorted() === 'desc' ? '↓' : '↑'}
                          </span>
                        )}
                      </>
                    )
                  }
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {rows.map(row => (
            <tr key={row.id} className="border-t hover:bg-slate-50">
              {row.getVisibleCells().map(cell => (
                <td key={cell.id} className="px-4 py-2 text-sm">
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

That's a production table: filter input synced to all columns, clickable headers for sorting, visual sort indicators, hover states. You own the markup so you can add row checkboxes, link names to detail pages, apply Tailwind utilities per cell, or swap to <div> grid for mobile. TanStack Table doesn't care — it just manages state.

Four Production Patterns

1. Row Selection + Bulk Actions

Select checkboxes and trigger a toolbar with delete, archive, export buttons:

const [rowSelection, setRowSelection] = useState({});

const table = useReactTable({
  ...config,
  state: { rowSelection, ...state },
  onRowSelectionChange: setRowSelection,
  enableRowSelection: true,
  getRowCanBeSelected: (row) => row.original.status !== 'archived',
});

const selectedCount = Object.values(rowSelection).filter(Boolean).length;

return (
  <div>
    {selectedCount > 0 && (
      <div className="flex gap-2 mb-4 p-3 bg-blue-50 rounded">
        <span>{selectedCount} selected</span>
        <button onClick={() => deleteSelected()}>Delete</button>
        <button onClick={() => exportSelected()}>Export CSV</button>
      </div>
    )}
    {/* table markup */}
  </div>
);

// In your table header:
<th>
  <input
    type="checkbox"
    checked={table.getIsAllRowsSelected()}
    onChange={(e) => table.toggleAllRowsSelected(e.target.checked)}
  />
</th>

// In your table body:
<td>
  <input
    type="checkbox"
    checked={row.getIsSelected()}
    onChange={(e) => row.toggleSelected(e.target.checked)}
  />
</td>

Select a few rows, the toolbar appears. Shift-click to select a range, Cmd-click to toggle. All row selection logic is built-in; you just render checkboxes and wire the handlers. Mobile? Same table, smaller cells, checkbox on the left stays sticky. No prop drilling through layers of configuration.

2. Virtual Scrolling for 50K Rows

Staff rosters can grow large. Virtual scroll only renders visible rows:

import { useVirtualizer } from '@tanstack/react-virtual';

const { rows } = table.getRowModel();
const parentRef = useRef(null);

const virtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 40,
  overscan: 10,
});

const virtualRows = virtualizer.getVirtualItems();
const totalSize = virtualizer.getTotalSize();

return (
  <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
    <table style={{ height: totalSize }}>
      <tbody>
        {virtualRows.map(virtualRow => (
          <tr
            key={virtualRow.key}
            style={{
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            {/* render row */}
          </tr>
        ))}
      </tbody>
    </table>
  </div>
);

50K rows in the database. Only 15–20 render at a time. Scroll is buttery even on mobile. Sorting and filtering work on the full dataset instantly — TanStack Table processes it in memory (or you wire server-side filtering for truly massive datasets).

3. Sticky Header + Mobile Horizontal Scroll

Desktop: sticky header stays visible when scrolling. Mobile: left columns sticky, table scrolls horizontally:

<div className="overflow-x-auto">
  <table className="min-w-full">
    <thead className="sticky top-0 bg-white dark:bg-slate-900 z-10">
      {/* headers */}
    </thead>
    <tbody>
      {/* rows */}
    </tbody>
  </table>
</div>

On mobile, wrap the table in an overflow-x div and your content scrolls. Add sticky left-0 to the first cell in each row and the name column stays visible while other columns scroll away. CSS Grid table (display: grid?) works too. TanStack Table is indifferent — markup is yours.

4. Column Visibility Toggle

Users pick which columns to see — email, role, or status:

const [columnVisibility, setColumnVisibility] = useState({
  email: true,
  role: true,
  status: true,
});

const table = useReactTable({
  ...config,
  state: { columnVisibility, ...state },
  onColumnVisibilityChange: setColumnVisibility,
});

return (
  <div className="space-y-4">
    <div className="flex gap-2">
      {table.getAllLeafColumns().map(column => (
        <label key={column.id} className="flex items-center gap-2">
          <input
            type="checkbox"
            checked={column.getIsVisible()}
            onChange={(e) => column.toggleVisibility(e.target.checked)}
          />
          {column.columnDef.header}
        </label>
      ))}
    </div>
    {/* table markup */}
  </div>
);

Check or uncheck columns and the table reflows. No data reload, no API call, instant responsive table. Store the visibility in localStorage if the user wants their preference saved across sessions.

Six FAQs

Is TanStack Table production-ready?

Yes. It powers thousands of SaaS dashboards (Vercel, Netlify, Supabase, Plaid, Stripe). The API is stable, the team is responsive, and there's no "v5 breaking everything" risk — major versions happen ~2–3 years apart. Rebuild Relief's staff tools and Staff Operations Dashboard analytics both use TanStack Table. Ship with confidence.

What about AG Grid Community vs Pro? Should I pay?

AG Grid Community is free with attribution. Pro adds server-side filtering, real-time cell updates, Excel export, and paid support. If you need those features (enterprise finance, real-time trading), the cost is justified. But for SaaS dashboards (staff, invoices, analytics), Community is a trap — you're paying for features you don't use and fighting the licensing. TanStack Table is free, MIT, no strings. Export to CSV with two lines of code.

How do I integrate TanStack Table with a server-side API?

TanStack Table is client-side state management. When you filter or sort, dispatch an API call with the new filter/sort params, update state, table re-renders. For pagination, you can use manual pagination (query params: ?page=2&limit=20) or infinite scroll (append rows to state as user scrolls). No special TanStack Table magic — just React state + fetch.

Can I use TanStack Table in Next.js with Server Components?

No. TanStack Table requires 'use client' and React hooks (useState, useCallback). Use it in a Client Component child, pass server-fetched data as a prop. The server loads the data, the client renders the interactive table. Common pattern: <StaffRosterPage> (server) loads roster from DB, passes it to <StaffTable> (client) which handles filtering/sorting.

Does TanStack Table work with TypeScript?

Yes. Full TypeScript support. Define your row shape as a type, pass it to useReactTable<YourRowType>, and you get autocomplete on all accessors and column definitions. Schema validation (Zod) before rendering makes this rock-solid.

What if I need a date picker in a table cell?

TanStack Table is state management. Cell content is JSX. Render a date picker (react-datepicker, radix Calendar, your own) inside the cell component. When the user picks a date, update row state, and TanStack Table re-renders. No special integration needed — markup is yours.

The Bottom Line

Pick your table library based on your constraints, not your ambitions. AG Grid owns enterprise: pivot tables, master-detail, real-time cells, Excel parity, if you pay the license. AG Grid Community is free but binds you to their component philosophy. Material-UI DataGrid is a solid middle ground if you're already on MUI. But TanStack Table is the fastest path to a custom, lightweight, performant table: headless state management, you own the markup, MIT license, 18KB bundle, virtual scroll out of the box. Pair it with custom table builds where your data interaction needs fine-grained control, and you've got a system that scales from day 1. Build the table in a day. Ship filters, sorting, and multi-select in an afternoon. Spend the week building the workflows around it instead.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.