Problem
ReUI's Data Grid works very well for client-side datasets, but there is currently no documented end-to-end pattern for a grid backed by a large server-side dataset.
There is also a subtle pagination footgun around recordCount.
<DataGrid> requires a recordCount:
<DataGrid
table={table}
recordCount={1_000_000}
>
<DataGridPagination />
</DataGrid>
DataGridPagination uses that value for the displayed record range:
but obtains the actual number of pages separately:
const pageCount = table.getPageCount()
With TanStack Table v9 and manualPagination: true, table.getPageCount() needs rowCount or pageCount on useTable() to know the total server-side result count.
So a natural implementation like this:
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 25,
})
const { data, total } = useCompanies(pagination)
const table = useTable({
features: dataGridFeatures,
columns,
data,
manualPagination: true,
state: {
pagination,
},
onPaginationChange: setPagination,
})
return (
<DataGrid table={table} recordCount={total}>
<DataGridTable />
<DataGridPagination />
</DataGrid>
)
can render:
while table.getPageCount() only sees the currently supplied page of rows, so the pagination controls do not know about the remaining pages.
The consumer has to know to duplicate the total:
const table = useTable({
// ...
manualPagination: true,
rowCount: total,
})
<DataGrid
table={table}
recordCount={total}
/>
This is easy to miss because recordCount already reads semantically as the source of truth for the total dataset size.
Why this matters
For a real data-heavy application, loading the entire dataset into the browser is often not an option.
Typical examples are:
- CRM / ERP records
- analytics applications
- financial datasets
- company / people databases
- logs and observability
- admin applications with hundreds of thousands or millions of rows
In these cases the backend normally owns:
- pagination
- sorting
- filtering
- global search
- faceting / option counts
and the Data Grid owns only the corresponding controlled UI state.
ReUI already has most of the pieces required for this. The core even exports API-oriented types:
export type DataGridApiFetchParams = {
pageIndex: number
pageSize: number
sorting?: SortingState
filters?: ColumnFiltersState
searchQuery?: string
}
export type DataGridApiResponse<T> = {
data: T[]
empty: boolean
pagination: {
total: number
page: number
}
}
but I could not find an example that consumes them.
Proposal
I see two separate improvements here.
1. Remove the recordCount / rowCount footgun
Either:
Option A — document the distinction explicitly
Add a server-side pagination example showing:
const table = useTable({
features: dataGridFeatures,
data: result.rows,
columns,
manualPagination: true,
rowCount: result.total,
state: { pagination },
onPaginationChange: setPagination,
})
return (
<DataGrid
table={table}
recordCount={result.total}
>
<DataGridTable />
<DataGridPagination />
</DataGrid>
)
and explain that:
DataGrid.recordCount powers ReUI's record-range UI;
useTable({ rowCount }) powers TanStack's pagination model.
Option B — let DataGrid use recordCount as the default TanStack rowCount
When manual pagination is enabled and neither an explicit rowCount nor pageCount was supplied, ReUI could default the table's row count to recordCount.
That would make the common case have one source of truth while preserving explicit TanStack configuration as an escape hatch.
I'm not sure whether mutating/inheriting table options from <DataGrid> fits the intended "consumer owns the table" architecture, so Option A may be preferable.
2. Add a complete server-side Data Grid example
A single example demonstrating the recommended v9 pattern would be extremely useful.
Something along the lines of:
const [pagination, setPagination] = useState(...)
const [sorting, setSorting] = useState(...)
const [columnFilters, setColumnFilters] = useState(...)
const [globalFilter, setGlobalFilter] = useState(...)
const result = useRemoteData({
pagination,
sorting,
columnFilters,
globalFilter,
})
const table = useTable({
features: dataGridFeatures,
columns,
data: result.rows,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
rowCount: result.total,
state: {
pagination,
sorting,
columnFilters,
globalFilter,
},
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
})
The example could use a mocked asynchronous API rather than introduce a dependency on TanStack Query.
It would ideally demonstrate:
- manual/server-side pagination;
- server-side sorting;
- server-side column filtering;
- global search;
- loading state while a request is in flight;
- resetting
pageIndex when filters/search change;
- stable
getRowId;
- total result count;
- empty results;
- preserving the previous page while fetching, if desired.
Related: remote filter values
#97 proposes dynamic option loading for Filters, which seems complementary rather than duplicative.
For very large datasets, the table rows and filter facets are often both remote:
GET /companies
?page=...
&sort=...
&filters=...
GET /companies/facets/industry
?search=manufact...
&filters=...
So a future server-side example could eventually compose naturally with #97.
Why an example may be enough
I don't think ReUI necessarily needs to become a data-fetching library or wrap TanStack Query.
The desirable property is that ReUI provides a clear composition contract between:
DataGrid UI state
↓
serializable query state
↓
application API
↓
rows + total
↓
DataGrid
while leaving transport, caching and backend implementation to the application.
A canonical example would remove a lot of ambiguity for people using ReUI for datasets that cannot live entirely in browser memory.
Problem
ReUI's Data Grid works very well for client-side datasets, but there is currently no documented end-to-end pattern for a grid backed by a large server-side dataset.
There is also a subtle pagination footgun around
recordCount.<DataGrid>requires arecordCount:DataGridPaginationuses that value for the displayed record range:but obtains the actual number of pages separately:
With TanStack Table v9 and
manualPagination: true,table.getPageCount()needsrowCountorpageCountonuseTable()to know the total server-side result count.So a natural implementation like this:
can render:
while
table.getPageCount()only sees the currently supplied page of rows, so the pagination controls do not know about the remaining pages.The consumer has to know to duplicate the total:
This is easy to miss because
recordCountalready reads semantically as the source of truth for the total dataset size.Why this matters
For a real data-heavy application, loading the entire dataset into the browser is often not an option.
Typical examples are:
In these cases the backend normally owns:
and the Data Grid owns only the corresponding controlled UI state.
ReUI already has most of the pieces required for this. The core even exports API-oriented types:
but I could not find an example that consumes them.
Proposal
I see two separate improvements here.
1. Remove the
recordCount/rowCountfootgunEither:
Option A — document the distinction explicitly
Add a server-side pagination example showing:
and explain that:
DataGrid.recordCountpowers ReUI's record-range UI;useTable({ rowCount })powers TanStack's pagination model.Option B — let
DataGriduserecordCountas the default TanStackrowCountWhen manual pagination is enabled and neither an explicit
rowCountnorpageCountwas supplied, ReUI could default the table's row count torecordCount.That would make the common case have one source of truth while preserving explicit TanStack configuration as an escape hatch.
I'm not sure whether mutating/inheriting table options from
<DataGrid>fits the intended "consumer owns the table" architecture, so Option A may be preferable.2. Add a complete server-side Data Grid example
A single example demonstrating the recommended v9 pattern would be extremely useful.
Something along the lines of:
The example could use a mocked asynchronous API rather than introduce a dependency on TanStack Query.
It would ideally demonstrate:
pageIndexwhen filters/search change;getRowId;Related: remote filter values
#97 proposes dynamic option loading for Filters, which seems complementary rather than duplicative.
For very large datasets, the table rows and filter facets are often both remote:
So a future server-side example could eventually compose naturally with #97.
Why an example may be enough
I don't think ReUI necessarily needs to become a data-fetching library or wrap TanStack Query.
The desirable property is that ReUI provides a clear composition contract between:
DataGrid UI state ↓ serializable query state ↓ application API ↓ rows + total ↓ DataGridwhile leaving transport, caching and backend implementation to the application.
A canonical example would remove a lot of ambiguity for people using ReUI for datasets that cannot live entirely in browser memory.