Capítulo 10 de 54
Every dataset-wide operation (filter, sort, group, paginate, aggregate) is either fully client-side (TanStack Table processes rows already in the browser) or fully server-side ("manual" — the table just holds state/APIs, your backend does the work) — and per the docs, you basically can't mix approaches across features for the same dataset without misleading results.
manual* flag doesn't fetch or transform anything — it tells the table "trust the data you gave me as already filtered/sorted/paginated for this feature." You can still register the feature (for its state/APIs) while omitting its row-model factory.| Operation | Manual flag | Client-side row model |
|---|---|---|
| Column/global filtering | manualFiltering | filteredRowModel |
| Grouping | manualGrouping | groupedRowModel |
| Aggregation | manualAggregation | local aggregationFn fallback |
| Sorting | manualSorting | sortedRowModel |
| Expanding | manualExpanding | expandedRowModel |
| Pagination | manualPagination | paginatedRowModel |
Faceting supports server-provided results too, but via custom factories rather than a manual* flag (see Faceting Guide).
// server-side page/sort/filter driven by TanStack Query — the general shape
const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, rowPaginationFeature, rowSortingFeature })
// no filteredRowModel/sortedRowModel/paginatedRowModel: server does that work
const dataQuery = useQuery({
queryKey: ['people', { sorting, globalFilter, pagination }],
queryFn: () => fetchPeople({ sorting, globalFilter, pagination }),
placeholderData: keepPreviousData,
})
const table = useTable({
features, columns,
data: dataQuery.data?.rows ?? [],
rowCount: dataQuery.data?.rowCount,
state: { sorting, globalFilter, pagination },
onSortingChange: (u) => { setSorting(u); setPagination(p => ({ ...p, pageIndex: 0 })) },
onGlobalFilterChange: (u) => { setGlobalFilter(u); setPagination(p => ({ ...p, pageIndex: 0 })) },
onPaginationChange: setPagination,
manualFiltering: true, manualSorting: true, manualPagination: true,
})
pageIndex back to 0 — otherwise you can land on a now-nonexistent page after a filter/sort change.Why you must reset pagination by hand in the change handlers: with a fully manual (server-side) config, the client-side filtered/sorted row models — which normally trigger an automatic page-index reset as a side effect of recomputing — are never registered, so their auto-reset hooks never fire. manualPagination also disables autoResetPageIndex by default. The fix is always the same shape: reset pageIndex to 0 inside onSortingChange/onGlobalFilterChange/onColumnFiltersChange, not as an afterthought.
For cursor-based pagination (when a total row count is expensive or a full jump-to-page isn't needed), pair useInfiniteQuery with pageCount: -1 and derive canNextPage from whether the next cursor page is cached or the last response reported hasNextPage — getCanNextPage() can't know the answer on its own when the total is unknown, and getCanLastPage() always returns false in that mode since there's no finite last page to target.
manual* flag.pageIndex in the change handlers for server-owned filter/sort/global-filter state under manualPagination — the automatic reset you'd get from a client-side row model doesn't fire.getRowId for server-driven tables — page-relative row indexes don't reliably identify the same record across requests/responses.