Online API
Use this page when you need the exact backend contract for server-driven tables. For the higher-level decision, start with Choose a data mode.
Main config
interface OnlineConfig<TData> {
mode: "infinite" | "pagination"
queryKey: readonly unknown[]
supportedGroupingColumns?: string[]
query: (input: OnlineQueryInput) => Promise<OnlineQueryResponse<TData>>
initialData?: OnlineQueryResponse<TData>
initialDataQueryState?: OnlineQueryStateInput
pageSize?: number
prefetchRows?: number
initialPageIndex?: number
}Online mode means the backend owns query semantics:
- global search
- column filters
- sorting
- grouping
- totals
- stable row ordering
- page or window slicing
The client still owns viewport rendering, row measurement, and which visible range should be loaded next.
Required fields
mode
"infinite" | "pagination"pagination: one page at a time, withpageIndexand pagination controlsinfinite: server windows addressed by data-rowoffset, which supports long scrolling and direct scrollbar jumps
queryKey
Stable query identity for this table.
Keep it stable across renders and specific enough to isolate tenant or table identity.
query
Your backend query function.
online={{
mode: "pagination",
queryKey: ["customers", workspaceId],
query: (input) => trpc.customer.listOnline.query(input),
}}This is the authoritative boundary between the datatable and your backend. The table sends interaction state; your backend returns fully ordered rows plus totals and optional grouping/facet metadata.
Optional fields
supportedGroupingColumns
List of column IDs your backend can group by.
When provided, the table sanitizes persisted and URL-loaded grouping state so unsupported columns do not stay selected or get sent to the server.
initialData
Optional prefetched first response.
Use this when the route loader or parent component already fetched the first online result before the table mounts.
initialDataQueryState
The exact query state that produced initialData.
If provided, the table only seeds initialData when the current filter/sort/search/grouping state still matches it. This prevents showing stale prefetched rows after persisted state or URL state changes the real query.
pageSize
Rows requested per server call.
- default:
50
prefetchRows
How aggressively online infinite mode warms data before and after the visible range.
- default:
pageSize
This is separate from virtualization overscan:
- virtualization overscan controls mounted DOM rows
prefetchRowscontrols server data warmed around the visible range
initialPageIndex
Initial page in pagination mode.
- default:
0 - ignored in infinite mode
Query input types
Shared query state
interface OnlineQueryStateInput {
limit: number
filters: ColumnFilter[]
sorting: SortingState
globalFilter: string
grouping?: {
columns: string[]
showEmptyGroups?: boolean
}
}The table always sends the current query semantics in this shape.
limit
Requested page or window size.
filters
Column filters in the datatable's own internal format.
sorting
TanStack-style sorting state.
globalFilter
Current quick-search string.
grouping
Present only when grouping is active.
{
columns: string[]
showEmptyGroups?: boolean
}columns: group-by columns in ordershowEmptyGroups: request zero-count groups when your backend knows a finite group domain
Navigation-specific input
Pagination input
interface PaginationOnlineQueryInput extends OnlineQueryStateInput {
mode: "pagination"
pageIndex: number
offset: number
}Example:
{
mode: "pagination",
limit: 50,
pageIndex: 0,
offset: 0,
filters: [],
sorting: [{ id: "company", desc: false }],
globalFilter: "",
}Infinite input
interface InfiniteOnlineQueryInput extends OnlineQueryStateInput {
mode: "infinite"
offset: number
}Example:
{
mode: "infinite",
limit: 50,
offset: 80000,
filters: [],
sorting: [{ id: "company", desc: false }],
globalFilter: "",
}Important: in infinite mode, offset is the first data-row index needed for the visible range. Requests may arrive out of order.
Response type
interface OnlineQueryResponse<TData> {
rows: OnlineTableRow<TData>[]
totalDataRows: number
totalRenderedRows: number
hasMore: boolean
grouping?: OnlineGroupingSummary
facets?: Record<string, Array<{ value: string; count: number }>>
}Response fields
rows
Rows in final render order.
Ungrouped mode returns only data rows. Grouped mode returns both structural group headers and data rows.
totalDataRows
Count of matching data records across the full filtered result.
totalRenderedRows
Count of full rendered rows across the filtered result, including structural rows such as group headers.
This matters for grouped pagination and grouped infinite mode.
hasMore
Whether more results remain beyond the current page or loaded window.
grouping
Optional full-query grouping summary used by the client to build the rendered row plan in grouped online mode.
facets
Optional aggregated filter counts keyed by column ID.
Facet counts should use the same tenant, permission, search, and filter scope as the returned rows.
Row model
type OnlineTableRow<TData> =
| {
type: "group-header"
groupId: string
columnId: string
value: string
depth: number
count: number
isExpanded?: boolean
groupPath: string[]
}
| {
type: "data"
rowId: string
item: TData
groupPath: string[]
}Group header rows
Use these when the backend returns grouped online output.
groupId: stable server-owned identifier for the groupcolumnId: grouped columnvalue: visible group valuedepth: grouping depth (0for primary group,1for subgroup, and so on)count: data-row count in the groupgroupPath: ancestor group IDs in order
Data rows
rowId: stable row identifieritem: your domain objectgroupPath: ancestor group IDs for grouped output, or[]when ungrouped
Grouping summary
interface OnlineGroupingSummary {
groups: Record<
string,
{
total: number
renderedRowCount: number
subgroups?: Record<
string,
{
total: number
renderedRowCount: number
}
>
}
>
}This summary describes the full grouped result, not just the currently loaded page.
Use it so the client can:
- build sparse rendered rows for infinite loading
- insert group headers in the right places
- preserve empty groups when requested
- translate visible rendered ranges back into data-row offsets
For grouped infinite mode, totalRenderedRows should include structural group rows for the whole filtered result, not only the returned slice.
Inspecting real query behavior in the showcase backend
The showcase /api/showcase/data route emits lightweight diagnostics headers so you can confirm which backend path actually ran without attaching a profiler.
server-timing: total request execution time for the datatable query pathx-react-datatable-query-plan:flat_windoworgrouped_windowx-react-datatable-query-path:flat_sqlorgrouped_sqlx-react-datatable-query-duration-ms: query execution duration in milliseconds
Use this to verify that real requests are taking the SQL-backed path and to spot regressions quickly during staging or production debugging.
Facets
facets: {
status: [
{ value: "active", count: 842 },
{ value: "trial", count: 91 },
],
}Keys should match column IDs.
Use facets when your filter UI needs backend-aware counts for the currently filtered/search-scoped result.
Server responsibilities
Apply the online query in a stable order:
- global search
- column filters
- group key ordering when grouping is active
- sorting
- grouping and group-header creation
- pagination or infinite offset slicing
- stable tie-breaker ordering
That stable ordering matters because online infinite mode may fetch windows out of order.
Infinite-loading behavior
The client does not fetch rows sequentially forever. It requests the visible data-row range plus prefetched nearby windows.
That means your backend should be comfortable receiving requests like:
- offset
0 - then offset
200 - then offset
80000
without assuming all prior pages were loaded first.
Empty groups
When grouping.showEmptyGroups is true, grouped responses may include empty groups if your backend knows the finite group domain.
A zero-count group can look like:
{
total: 0,
renderedRowCount: 1
}The included local server helper can derive this from static filterOptions.options domains. Production backends should implement the equivalent behavior from their own enums or domain tables.
Helper export
react-datatable-server exports runInMemoryDatatableQuery.
Use it for:
- demos
- fixtures
- tests
- local examples
For production, implement the same contract against your real database or ORM instead of relying on the helper as a hosted abstraction.
See Server helper API.
Practical reminders
- Keep
rowIdandgroupIdstable. - Treat online offsets as data-row offsets, not rendered-row offsets.
- Return rows in final render order.
- Keep facet counts and totals scoped exactly the same way as the row result.
- Do not confuse virtualization with server pagination; one is rendering policy, the other is data ownership.