react-datatable
Reference

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, with pageIndex and pagination controls
  • infinite: server windows addressed by data-row offset, 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
  • prefetchRows controls 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 order
  • showEmptyGroups: request zero-count groups when your backend knows a finite group domain

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 group
  • columnId: grouped column
  • value: visible group value
  • depth: grouping depth (0 for primary group, 1 for subgroup, and so on)
  • count: data-row count in the group
  • groupPath: ancestor group IDs in order

Data rows

  • rowId: stable row identifier
  • item: your domain object
  • groupPath: 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 path
  • x-react-datatable-query-plan: flat_window or grouped_window
  • x-react-datatable-query-path: flat_sql or grouped_sql
  • x-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:

  1. global search
  2. column filters
  3. group key ordering when grouping is active
  4. sorting
  5. grouping and group-header creation
  6. pagination or infinite offset slicing
  7. 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 rowId and groupId stable.
  • 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.

On this page