A schematic of one line: the client/server boundary. Every library on this sheet draws the same boundary — they differ only in how much of the fetch/cache/lazy-load machinery they place on which side of it, and how much of it they leave for you to fabricate on site.
If the server owns pagination + sorting + filtering and grouping/aggregation, only AG Grid's Server-Side Row Model [12] and MUI X's Data Source [18] are first-class engines that fetch, cache and lazy-load for you — both paywalled at the group/aggregate tier [13][20]. TanStack Table ⭐ 28.2k gives you manual* opt-outs, not an engine: you own the state, the fetch, the debounce and the page-reset [1][5] — pair it with TanStack Query ⭐ 50k and it's excellent, but the wiring is yours. Mantine React Table ⭐ 1.1k inherits exactly TanStack Table v8's manual model and adds nothing server-side [24].
TanStack Table, Mantine React Table — you flip manualPagination / manualSorting / manualFiltering and the table stops doing the work. It does not start doing it on the server. It just believes whatever data array you hand it [1][2][3].
fetchRows()youuseReactTable — renders rows, believes datalibGET /rows?page&sort&filteryou{ rows, total }youAG Grid SSRM, MUI X Data Source — you implement one getRows(params) callback. The grid owns the request lifecycle, block cache, scroll-triggered fetching, loading skeletons and error surface [12][14][18].
cacheBlockSize 100, LRU evictiongridblockLoadDebounceMillis / 500 ms throttlegridgetRows(params) — the one seam you fabricateyoustartRow / endRowreqsortModel · filterModelreqrowGroupCols · groupKeysreqvalueCols · pivotColsreqaggregationModel (MUI)reqpaginationMode="server" or Data Source [21]manualGrouping exists, but "not currently many known easy ways" [4]useInfiniteQuery + Virtual [7]lazyLoading prop [19]GridDataSourceCacheDefault, 5-min TTL, swappable [18]blockLoadDebounceMillis [15]'use client''use client''use client'The features that move work across the line are exactly the features that are gated. Grouping and aggregation on the server are not something you can grow into on a free tier — they are the purchase.
paginationMode/sortingMode/filterMode="server", basic dataSourceTurn on manualPagination and autoResetPageIndex is switched off for you [1]. Nothing in the table connects a filter change to the page index. The failure is silent — no error, no empty-state, just an empty table.
pageIndex stays at 999[]. Table renders empty. User concludes the filter matched nothing.Open since v8 — TanStack Table issue #4797 ⭐ 28.2k [5]. The fix is yours to fabricate: wrap setFilters / setSorting so they also setPagination(p => ({ ...p, pageIndex: 0 })) — in the same transaction, so only one query key ever exists.
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); const [sorting, setSorting] = useState<SortingState>([]); const [filters, setFilters] = useState<ColumnFiltersState>([]); const q = useQuery({ queryKey: ['rows', pagination, sorting, filters], // the key IS the query queryFn: () => fetchRows({ pagination, sorting, filters }), placeholderData: keepPreviousData, // no flash-to-empty }); const table = useReactTable({ data: q.data?.rows ?? [], columns, getCoreRowModel: getCoreRowModel(), manualPagination: true, manualSorting: true, manualFiltering: true, rowCount: q.data?.total, state: { pagination, sorting, columnFilters: filters }, onPaginationChange: setPagination, onSortingChange: setSorting, onColumnFiltersChange: setFilters, });
const datasource: IServerSideDatasource = { getRows: params => { const res = server.getData(params.request); res.success ? params.success({ rowData: res.rows, rowCount: res.total }) : params.fail(); }, };
getRows is a plain callback, not a component, so you cannot call useQuery inside it. Use useQueryClient() in the component and qc.fetchQuery(...) inside getRows, memoise the datasource, and put every request param in the key [17]. You are then running two caches — decide which is authoritative before you build.React's own docs are blunt: "network responses may arrive in a different order than you sent them" — and then tell you not to hand-roll the fix at all, but to use TanStack Query or SWR [30].
{ pagination, sorting, filters } into the query key structurally removes the race: each parameter combination is a distinct cache entry, so a late response writes into its own key, not the visible one [8].pageIndex in the same transaction as the filter/sort change [5].keepPreviousData and isPreviousData; write placeholderData: keepPreviousData and read isPlaceholderData [9]. Gate the Next button on isPlaceholderData || !data.hasMore [8].rowCount becomes undefined during loading, it will reset the page to zero" — memoise it across fetches [21].rowCount → MUI picks viewport loading: skeleton rows, fetch when a skeleton enters the render window, 500 ms request throttle [19]. In TanStack land this is a plain useQuery keyed on the page.rowCount = -1 (or undefined) plus paginationMeta.hasNextPage or estimatedRowCount → infinite loading: fetch at scroll-end, stop when the server returns nothing [19][21]. In TanStack land: useInfiniteQuery + getNextPageParam returning the server's nextCursor, with maxPages to cap retained pages [10].SWR does the same job with one option — keepPreviousData: true keeps prior data across key changes [11] — but has no equivalent of Query's imperative fetchQuery escape hatch, so for AG Grid SSRM and MUI Data Source, Query is the better fit.
All four need 'use client', and once a file is marked, "all of its imports and the components it directly renders are included in the client bundle" [29]. AG Grid says it outright: ag-grid-react "depends on some browser-specific APIs (e.g. window/document) and can not be rendered server-side" [16]. So the boundary you draw in the App Router is URL-as-table-state:
page.tsx parses searchParams (a Promise since Next 15) via createSearchParamsCache and passes typed page/sort/filter down without prop-drilling [26].'use client' table shell.useQueryState; shallow: false opts into "notifying the server (to re-render Server Components on the app router)", with throttleMs to rate-limit — the hook's state updates instantly regardless, only the URL write and server round-trip are throttled (default 50 ms; ~340 ms on Safari) [27]. That throttleMs is your debounce for RSC round-trips.Reference implementation: shadcn-table ⭐ 6.2k — TanStack Table + Next.js with "server-side pagination, sorting, and filtering", Notion-style advanced filters and virtualized infinite scroll [28]. AG Grid and MUI X can live inside this too, but their state lives in the grid's own model objects, so you must serialise sortModel/filterModel into the URL yourself — the URL-state ergonomics favour the headless option.
| Your situation | Specified component |
|---|---|
| Flat rows, offset or cursor paging, sort + filter, no grouping | TanStack Table + TanStack Query — manual* + placeholderData: keepPreviousData [1][8] |
| Table state must live in the URL / RSC + App Router | TanStack Table + nuqs — grid-model libraries fight you here [26][28] |
| Server-side grouping, pivot, aggregation over millions of rows | AG Grid SSRM (Enterprise) [12][13] |
| Already on MUI, want server grouping/aggregation without writing the engine | MUI X Data Source — Pro for lazy load, Premium for aggregation [19][20] |
| Mantine design system, simple server paging | Mantine React Table — but it is a TanStack v8 wrapper with a stale cadence (last tagged release Oct 2023) [25]. ⚠ No server-side story of its own. |
Revision note — v9. TanStack Table v9 has been in beta since 7 June 2026 (TanStack Store-backed state, plugin-registered features, large memory and type-instantiation wins). The announcement says nothing about improving server-driven mode — the manual* opt-out philosophy is unchanged [6]. Adopting v9 in 2026 means adopting a beta.
Only four candidates are genuinely alive in 2026, and the real axis is not features but how many weeks of table you agree to write yourself.
Row count is almost never the constraint — update frequency, referential stability and the browser's 33.5M-pixel scroll cap are.
AG Grid's Server-Side Row Model and MUI X's Data Source are real server-driven engines; TanStack Table only gives you manual* opt-outs and hands you the state machine.
AG Grid Enterprise at $999/dev perpetual is the cheapest way to buy row grouping + pivot + Excel export; buy wins below ~40 developers.
If the design system is authoritative, go headless; MUI X / Mantine grids are design-system adoptions, not component adoptions.