Skip to content

Basics

Loading example…

A table in client mode holds all the rows and computes everything itself. A table in server mode holds one page and asks the API. Both are the same component with the same props; what changes is who does the work.

<DataTable data={people} columns={columns} getRowId={(p) => p.id} />

<DataTable dataMode="server" dataSource={dataSource} columns={columns} getRowId={(p) => p.id} />

dataMode="server" switches pagination, sorting, filtering, search, grouping and faceting to the server at once. Expansion stays local, because sub-rows can still come from the data you already have.

The query

Whichever mode you are in, the table maintains one normalized query: page, sort list, search term, column filters and grouping. That object is what a server mode table sends, and you can read it at any time with table.getQuery().

onQueryChange(query, change) fires once, after debouncing, whenever something that affects server data changes, and it tells you why it changed. It is the single place to hang a fetch, instead of six callbacks racing each other.

<DataTable
  dataMode="server"
  onQueryChange={(query) => void refetch(query)}
  data={page.items}
  rowCount={page.total}
/>

The per-slice callbacks (onSortingChange, onPaginationChange and the rest) still fire immediately, for controlled state.

A data source instead of an effect

dataSource packages the fetch, so the table drives the request itself and gets loading, error, refetch and facet handling for free:

interface PeoplePage {
  content: Person[];
  totalElements: number;
}

const dataSource = createRestDataSource<Person, PeoplePage>({
  url: '/api/people',
  mapQuery: (query) => ({
    page: query.pagination.pageIndex,
    size: query.pagination.pageSize,
    sort: query.sorting.map((s) => s.id + ',' + (s.desc ? 'desc' : 'asc')),
    q: query.globalFilter,
  }),
  mapResult: (body) => ({ rows: body.content, rowCount: body.totalElements }),
});

mapQuery turns the table's query into the request, and mapResult turns the response into rows and a total. The second type argument is the response's shape, which is what lets mapResult read it.

keepPreviousData keeps the old page visible while the next one loads, dedupeMs collapses duplicate requests, and refetchInterval and refetchOnWindowFocus cover live data.

Mixing the two

Loading example…

Per-feature modes override dataMode, so you can page and filter on the server while sorting the loaded page in the browser:

<DataTable dataMode="server" sortingMode="client" acknowledgePageLocalSorting />

That combination sorts the current page only, which is sometimes exactly what you want and sometimes a bug. The table warns about it in development until you acknowledge it in the props, which is also the documentation of the decision for whoever reads the code next.

Customization

createLocalDataSource gives an in-memory array the same interface as a REST source, which makes tests and prototypes behave like production. useDataSource exposes the same machinery outside a table, and useDataTable gives you the whole engine with no markup.

Limitations

  • Server grouping with client pagination is not supported and throws in development: the table cannot slice group rows it did not build.
  • Client faceting under server pagination describes the loaded page, not the result set. Use fetchFacets; see Faceting.
  • A server that does not return a total cannot produce page numbers. Pass rowCount: -1 and the table shows of many and switches to cursor-style navigation.

API

  • DataTableProps: dataMode, dataSource, paginationMode, sortingMode, filterMode, searchMode, groupingMode, facetingMode, the manual* flags, onQueryChange, rowCount, pageCount, keepPreviousData, dedupeMs, fetchOnMount, refetchInterval, refetchOnWindowFocus, acknowledgePageLocalSorting, acknowledgePageLocalFiltering.
  • TableState: TableQuery and every state slice.
  • TableInstance: getQuery, flushQuery, refresh, invalidate, getDataStatus.
  • Utilities: createRestDataSource, createLocalDataSource.