Delegate sorting, filtering and pagination to your API with a single dataProvider prop — the React Data Grid fetches one debounced request per state change and drops stale responses.
Client-side mode ends where the browser’s memory does. When your dataset lives
in a database with tens of thousands of rows or more, hand the work to your
backend: set dataProvider and the grid stops computing row models locally and
asks your API for each page instead. This is the single biggest adoption
unlocker for large data.
Server-side data
Set dataProvider and the grid switches to server-side mode — every sorting,
filtering or pagination change triggers one debounced request (stale responses
are dropped automatically), a loading overlay covers the viewport and totals
come from your totalRows. Server-side mode implies pagination. This live demo
hits CoreUI’s public demo API (apitest.coreui.io/demos/users) — 10,000+
records sorted, filtered and paged server-side.
import { CDataGrid } from '@coreui/react-data-grid'
import type { DataGridDataRequest } from '@coreui/react-data-grid'
import { useCallback } from 'react'
const fetchUsers = async ({ sorting, columnFilters, pagination }: DataGridDataRequest) => {
const params = new URLSearchParams({
offset: String(pagination.pageIndex * pagination.pageSize),
limit: String(pagination.pageSize)
})
for (const { id, value } of columnFilters) {
params.append(id, String(value))
}
const [sort] = sorting
if (sort) {
params.append('sort', `${sort.id}%${sort.desc ? 'desc' : 'asc'}`)
}
const response = await fetch(`https://apitest.coreui.io/demos/users?${params}`)
const result = await response.json()
const totalRows = Number(result.number_of_matching_records)
return { items: totalRows ? result.records : [], totalRows }
}
export const DataGridServerExample = () => {
const dataProvider = useCallback(fetchUsers, [])
return (
<CDataGrid
columns={[
{ key: 'first_name', label: 'First name' },
{ key: 'last_name', label: 'Last name' },
{ key: 'email', label: 'Email' },
{ key: 'country', label: 'Country' },
{ key: 'ip_address', label: 'IP' }
]}
itemKey={item => String(item.id)}
columnFilters
dataProvider={dataProvider}
/>
)
} import { CDataGrid } from '@coreui/react-data-grid'
import type { DataGridDataRequest } from '@coreui/react-data-grid'
import { useCallback } from 'react'
const fetchUsers = async ({ sorting, columnFilters, pagination }: DataGridDataRequest) => {
const params = new URLSearchParams({
offset: String(pagination.pageIndex * pagination.pageSize),
limit: String(pagination.pageSize)
})
for (const { id, value } of columnFilters) {
params.append(id, String(value))
}
const [sort] = sorting
if (sort) {
params.append('sort', `${sort.id}%${sort.desc ? 'desc' : 'asc'}`)
}
const response = await fetch(`https://apitest.coreui.io/demos/users?${params}`)
const result = await response.json()
const totalRows = Number(result.number_of_matching_records)
return { items: totalRows ? result.records : [], totalRows }
}
export const DataGridServerExample = () => {
const dataProvider = useCallback(fetchUsers, [])
return (
<CDataGrid
columns={[
{ key: 'first_name', label: 'First name' },
{ key: 'last_name', label: 'Last name' },
{ key: 'email', label: 'Email' },
{ key: 'country', label: 'Country' },
{ key: 'ip_address', label: 'IP' }
]}
itemKey={item => String(item.id)}
columnFilters
dataProvider={dataProvider}
/>
)
} The dataProvider contract
<CDataGrid
columns={columns}
dataProvider={async ({ sorting, columnFilters, globalFilter, pagination }) => {
// fetch from your API and return the matching page
return { items, totalRows }
}}
pagination // server-side mode implies pagination
/>- One request per state change.
sorting,columnFilters,globalFilterandpaginationarrive as structured state; return the page ofitemsplus the fulltotalRowscount so the pager can render. - Debounced & race-safe. Rapid changes coalesce into one request and only the latest response is applied — stale ones are dropped by request id.
- Loading UX. A
.data-grid-loadingoverlay with a spinner covers the viewport andaria-busyis set while a request is in flight (labels.loading). - Errors. A rejected fetch calls
onDataErrorwith theerror, shows the empty state (labels.loadError) and leaves the grid interactive. - Success. Each load calls
onDataLoadwith{ items, totalRows }.
Selection semantics
rowSelection is keyed by itemKey, so a selection survives
page changes by design. onSelectionChange’s selectedItems contains only the
items present in the current page’s data — the grid does not cache full objects
for pages it has scrolled past.
The same page-bound rule applies to CSV export: in
server-side mode every scope (filtered, all, selected) exports the
currently loaded page only.
When the filter menu is used, a column’s entry in
columnFilters carries the structured value — { conditions: [{ operator, value, value2? }], join: 'and' | 'or' } for typed operators or { operator: 'in', value: [...] } for the set filter — instead of a plain string. Servers
should handle both shapes.