Data Grid - Server-side data 🧪
The Data Grid server-side data.
Introduction
Server-side data management in React can become complex with growing datasets. Challenges include manual data fetching, pagination, sorting, filtering, and performance optimization. A dedicated module can help abstract these complexities, improving user experience.
Consider a Data Grid displaying a list of users. It supports pagination, sorting by column headers, and filtering. The Data Grid fetches data from the server when the user changes the page or updates filtering or sorting.
const [rows, setRows] = React.useState([]);
const [paginationModel, setPaginationModel] = React.useState({
page: 0,
pageSize: 10,
});
const [filterModel, setFilterModel] = React.useState({ items: [] });
const [sortModel, setSortModel] = React.useState([]);
React.useEffect(() => {
const fetcher = async () => {
// fetch data from server
const data = await fetch('https://my-api.com/data', {
method: 'GET',
body: JSON.stringify({
page: paginationModel.page,
pageSize: paginationModel.pageSize,
sortModel,
filterModel,
}),
});
setRows(data.rows);
};
fetcher();
}, [paginationModel, sortModel, filterModel]);
<DataGridPro
columns={columns}
pagination
sortingMode="server"
filterMode="server"
paginationMode="server"
onPaginationModelChange={setPaginationModel}
onSortModelChange={setSortModel}
onFilterModelChange={setFilterModel}
/>;
This example only scratches the surface with a lot of problems still unsolved like:
- Performance optimization
- Caching data/deduping requests
- More complex use cases on the server like grouping, tree data, etc.
- Server-side row editing
- Lazy loading of data
- Handling updates to the data like row editing, row deletion, etc.
- Refetching data on-demand
Trying to solve these problems one after the other can make the code complex and hard to maintain.
Data source
The idea for a centralized data source is to simplify server-side data fetching. It's an abstraction layer between the Data Grid and the server, providing a simple interface for interacting with the server. Think of it like an intermediary handling the communication between the Data Grid (client) and the actual data source (server).
It has an initial set of required methods that you need to implement. The Data Grid will use these methods internally to fetch a subset of data when needed.
Let's take a look at the minimal GridDataSource
interface configuration.
interface GridDataSource {
/**
* This method will be called when the grid needs to fetch some rows.
* @param {GridGetRowsParams} params The parameters required to fetch the rows.
* @returns {Promise<GridGetRowsResponse>} A promise that resolves to the data of
* type [GridGetRowsResponse].
*/
getRows(params: GridGetRowsParams): Promise<GridGetRowsResponse>;
}
Here's how the above mentioned example would look like when implemented with the data source:
const customDataSource: GridDataSource = {
getRows: async (params: GridGetRowsParams): GetRowsResponse => {
const response = await fetch('https://my-api.com/data', {
method: 'GET',
body: JSON.stringify(params),
});
const data = await response.json();
return {
rows: data.rows,
rowCount: data.totalCount,
};
},
}
<DataGridPro
columns={columns}
unstable_dataSource={customDataSource}
pagination
/>
The code has been significantly reduced, the need for managing the controlled states is removed, and data fetching logic is centralized.
Server-side filtering, sorting, and pagination
The data source changes how the existing server-side features like filtering
, sorting
, and pagination
work.
Without data source
When there's no data source, the features filtering
, sorting
, pagination
work on client
by default.
In order for them to work with server-side data, you need to set them to server
explicitly and provide the onFilterModelChange
, onSortModelChange
, onPaginationModelChange
event handlers to fetch the data from the server based on the updated variables.
<DataGrid
columns={columns}
rows={rows}
pagination
sortingMode="server"
filterMode="server"
paginationMode="server"
onPaginationModelChange={(newPaginationModel) => {
// fetch data from server
}}
onSortModelChange={(newSortModel) => {
// fetch data from server
}}
onFilterModelChange={(newFilterModel) => {
// fetch data from server
}}
/>
With data source
With the data source, the features filtering
, sorting
, pagination
are automatically set to server
.
When the corresponding models update, the Data Grid calls the getRows
method with the updated values of type GridGetRowsParams
to get updated data.
<DataGridPro
columns={columns}
// automatically sets `sortingMode="server"`, `filterMode="server"`, `paginationMode="server"`
unstable_dataSource={customDataSource}
/>
The following demo showcases this behavior.
Data caching
The data source caches fetched data by default.
This means that if the user navigates to a page or expands a node that has already been fetched, the grid will not call the getRows
function again to avoid unnecessary calls to the server.
The GridDataSourceCacheDefault
is used by default which is a simple in-memory cache that stores the data in a plain object. It can be seen in action in the demo above.
Improving the cache hit rate
To increase the cache hit rate, Data Grid splits getRows()
results into chunks before storing them in cache.
For the requests that follow, chunks are combined as needed to recreate the response.
This means that a single request can make multiple calls to the get()
or set()
method of GridDataSourceCache
.
Chunk size is the lowest expected amount of records per request based on the pageSize
value from the paginationModel
and pageSizeOptions
props.
Because of this, values in the pageSizeOptions
prop play a big role in the cache hit rate.
We recommend using values that are multiples of the lowest value; even better if each subsequent value is a multiple of the previous value.
Here are some examples:
Best scenario -
pageSizeOptions={[5, 10, 50, 100]}
In this case the chunk size is 5, which means that with
pageSize={100}
there are 20 cache records stored.Retrieving data for any other
pageSize
up to the first 100 records results in a cache hit, since the whole dataset can be made of the existing chunks.Parts of the data missing -
pageSizeOptions={[10, 20, 50]}
Loading the first page with
pageSize={50}
results in 5 cache records. This works well withpageSize={10}
, but not as well withpageSize={20}
. Loading the third page withpageSize={20}
results in a new request being made, even though half of the data is already in the cache.Incompatible page sizes -
pageSizeOptions={[7, 15, 40]}
In this situation, the chunk size is 7. Retrieving the first page with
pageSize={15}
creates chunks split into[7, 7, 1]
records. Loading the second page creates 3 new chunks (again[7, 7, 1]
), but now the third chunk from the first request has an overlap of 1 record with the first chunk of the second request. These chunks with 1 record can only be used as the last piece of a request forpageSize={15}
and are useless in all other cases.
Customize the cache lifetime
The GridDataSourceCacheDefault
has a default Time To Live (ttl
) of 5 minutes. To customize it, pass the ttl
option in milliseconds to the GridDataSourceCacheDefault
constructor, and then pass it as the unstable_dataSourceCache
prop.
import { GridDataSourceCacheDefault } from '@mui/x-data-grid-pro';
const lowTTLCache = new GridDataSourceCacheDefault({ ttl: 1000 * 10 }); // 10 seconds
<DataGridPro
columns={columns}
unstable_dataSource={customDataSource}
unstable_dataSourceCache={lowTTLCache}
/>;
Custom cache
To provide a custom cache, use unstable_dataSourceCache
prop, which could be either written from scratch or based on another cache library.
This prop accepts a generic interface of type GridDataSourceCache
.
export interface GridDataSourceCache {
set: (key: GridGetRowsParams, value: GridGetRowsResponse) => void;
get: (key: GridGetRowsParams) => GridGetRowsResponse | undefined;
clear: () => void;
}
Disable cache
To disable the data source cache, pass null
to the unstable_dataSourceCache
prop.
<DataGridPro
columns={columns}
unstable_dataSource={customDataSource}
unstable_dataSourceCache={null}
/>
Error handling
You could handle the errors with the data source by providing an error handler function using the unstable_onDataSourceError
.
It will be called whenever there's an error in fetching the data.
The first argument of this function is the error object, and the second argument is the fetch parameters of type GridGetRowsParams
.
<DataGridPro
columns={columns}
unstable_dataSource={customDataSource}
unstable_onDataSourceError={(error, params) => {
console.error(error);
}}
/>
Updating data 🚧
This feature is yet to be implemented, when completed, the method unstable_dataSource.updateRow
will be called with the GridRowModel
whenever the user edits a row.
It will work in a similar way as the processRowUpdate
prop.
Feel free to upvote the related GitHub issue to see this feature land faster.