Data Grid - Server-side data
Learn how to work with server-side data in the Data Grid using the Data Source layer.
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 to improve the developer experience. The Data Grid provides the Data Source layer for this purpose.
The problem: compounding complexity
Consider a Data Grid displaying a list of users that 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. Here's an example of what that might look like:
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]);
<DataGrid
columns={columns}
pagination
sortingMode="server"
filterMode="server"
paginationMode="server"
onPaginationModelChange={setPaginationModel}
onSortModelChange={setSortModel}
onFilterModelChange={setFilterModel}
/>;
But this example only scratches the surface of the complexity when working with server-side data in the Data Grid—the following features are still not addressed:
- Performance optimization
- Data caching and request deduping
- Row grouping and tree data
- Lazy-loading data
- Handling updates to the data (row editing and deletion)
- On-demand data refetching
Trying to tackle each of these features invidually can make the code overly complex and difficult to maintain.
The solution: the Data Source layer
For the Data Grid, the solution to the situation described above is a centralized abstraction layer called the Data Source. This provides an interface for communications between the Data Grid on the client and the actual data on the server.
The Data Source has an initial set of required methods that you must implement. The Data Grid uses these methods internally to fetch subsets of data as needed.
The following code snippet illustrates a minimal GridDataSource
interface configuration.
More complex implementations with properties like getChildrenCount()
and getGroupKey()
are discussed in the specific server-side feature docs that follow this introductory page.
interface GridDataSource {
/**
* This method is called when the grid needs to fetch 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 what the component from the aforementioned problem looks 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,
};
},
}
<DataGrid
columns={columns}
dataSource={customDataSource}
pagination
/>
With the Data Source, the total amount of code is significantly reduced; there's no need to manage controlled states; and data fetching logic is centralized.
Server-side filtering, sorting, and pagination
The Data Source changes how existing server-side features like filtering, sorting, and pagination work.
Without the Data Source (default behavior)
Without the Data Source implemented, features like filtering, sorting, pagination are designed to work on the client side by default.
To work correctly with server-side data, you must explicitly set these features to "server"
mode and provide the onFilterModelChange()
, onSortModelChange()
, and onPaginationModelChange()
event handlers to fetch the data from the server based on the updated variables, as shown below:
<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 the Data Source
With the Data Source implemented, features like filtering, sorting, and pagination are automatically set to "server"
mode.
When the corresponding models update, the Data Grid calls the getRows()
method with the updated values of type GridGetRowsParams
to get updated data.
<DataGrid
columns={columns}
// automatically sets `sortingMode="server"`, `filterMode="server"`, `paginationMode="server"`
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.
By default, the Grid uses GridDataSourceCacheDefault
, which is a simple in-memory cache that stores the data in a plain object.
You can see it working in the Data Source demo above.
Improve the cache hit rate
To increase the cache hit rate, the Data Grid splits getRows()
results into chunks before storing them in the 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.
As a result, the values in the pageSizeOptions
prop play an important 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-case 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 otherpageSize
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 three 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.
In the examples above, sortModel
and filterModel
remain unchanged.
Changing these would require a new response to be retrieved and stored in the chunks.
Customize the cache lifetime
The GridDataSourceCacheDefault
has a default time to live (TTL) of 5 minutes.
To customize this, pass the ttl
option with a numerical value (in milliseconds) to the GridDataSourceCacheDefault
constructor, then pass that to the dataSourceCache
prop.
import { GridDataSourceCacheDefault } from '@mui/x-data-grid';
const lowTTLCache = new GridDataSourceCacheDefault({ ttl: 1000 * 10 }); // 10 seconds
<DataGrid
columns={columns}
dataSource={customDataSource}
dataSourceCache={lowTTLCache}
/>;
Create a custom cache
Use the dataSourceCache
prop to provide a custom cache, which may be written from scratch or based on a third-party 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 caching
To disable the Data Source cache, pass null
to the dataSourceCache
prop.
<DataGrid columns={columns} dataSource={customDataSource} dataSourceCache={null} />
Updating server-side data
The Data Source supports an optional updateRow()
method for updating data on the server.
This method returns a promise that resolves when the row is updated.
If the promise resolves, the Grid updates the row and mutates the cache.
If there's an error, onDataSourceError()
is triggered with the error object containing the params described in the Error handling section that follows.
const dataSource: GridDataSource = {
getRows: async (params: GridGetRowsParams) => {
// fetch rows from the server
},
+ updateRow: async (params: GridUpdateRowParams) => {
+ // update row on the server
+ },
}
Error handling
You can handle errors with the Data Source by providing an error handler function with onDataSourceError()
.
This gets called whenever there's an error in fetching or updating the data.
This function recieves an error object of type GridGetRowsError | GridUpdateRowError
.
Each error type has a corresponding error.params
type which is passed as an argument to the callback:
Error type | error.params type |
---|---|
GridGetRowsError |
GridGetRowsParams |
GridUpdateRowError |
GridUpdateRowParams |
<DataGrid
columns={columns}
dataSource={customDataSource}
onDataSourceError={(error) => {
if (error instanceof GridGetRowsError) {
// `error.params` is of type `GridGetRowsParams`
// fetch related logic, e.g set an overlay state
}
if (error instanceof GridUpdateRowError) {
// `error.params` is of type `GridUpdateRowParams`
// update related logic, e.g set a snackbar state
}
}}
/>
The demo below renders a custom Snackbar component to display an error message when the requests fail, which you can simulate using the checkbox and the Refetch rows button at the top. Caching has been disabled for simplicity.