Skip to content

feat(data-grid): first-class server-side pagination / sorting / filtering pattern #126

Description

@LouisDeconinck

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:

1 - 25 of 1,000,000

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:

1 - 25 of 1,000,000

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions