AG-Grid React Series #37: Mastering the Server-Side Row Model
Exploring the capabilities of AG-Grid's Server-Side Row Model (SSRM) in React applications for handling vast datasets efficiently.
Welcome back to our AG-Grid React series! In this installment, we're diving deep into one of AG-Grid's most powerful features for enterprise-grade applications: the Server-Side Row Model (SSRM). If you've ever dealt with massive datasets that simply won't fit into client-side memory, or require complex server-side processing for aggregation and filtering, the SSRM is your go-to solution.
Unlike client-side row models that load all data into the browser, the Server-Side Row Model intelligently fetches data in chunks as needed, delegating operations like sorting, filtering, and even grouping and aggregation to your backend server. This approach ensures optimal performance and scalability, making your React applications responsive even with millions of rows.
What is the Server-Side Row Model (SSRM)?
At its core, the Server-Side Row Model is designed to handle data that lives on a remote server. Instead of the grid requesting all data upfront, it communicates with your backend via a custom data source. When the user scrolls, sorts, filters, or groups, the grid sends a request to your server detailing exactly what data it needs. Your server then processes this request, queries its database, and returns only the relevant subset of data.
This model is particularly crucial for:
- Huge Datasets: Where loading all data into the browser is impractical or impossible.
- Performance: Keeping the client-side lean and responsive.
- Security & Business Logic: Centralizing complex data operations, security checks, and business rules on the server.
- Scalability: Easily scaling your data infrastructure independent of the frontend.
Key Concepts and Architecture
Implementing the SSRM involves two primary components:
-
The AG-Grid Configuration (Frontend - React): You instruct the grid to use the 'serverSide' row model and provide it with an implementation of the
IServerSideDatasourceinterface. -
Your Server-Side Endpoint: This API endpoint receives requests from the grid, processes them (e.g., applies sorting, filtering, pagination), and returns the requested data.
The IServerSideDatasource Interface
The frontend's link to your backend is the IServerSideDatasource. It has one crucial method:
interface IServerSideDatasource {
getRows(params: IServerSideGetRowsParams): void;
// Optional: init(), destroy() for setup/teardown
}
The getRows method is called by AG-Grid whenever it needs new data. The params object passed to it contains all the information your server needs to fulfill the request:
request: An object detailing the current state of the grid (startRow, endRow, sortModel, filterModel, rowGroupCols, valueCols, pivotCols, pivotMode, etc.).successCallback(rowsThisBlock: any[], lastRow: number): Call this function with the data received from your server.rowsThisBlockis the array of rows for the current block, andlastRowindicates the total number of rows if known, or -1 if unknown (for infinite scrolling).failCallback(): Call this if there's an error fetching data.
Setting Up AG-Grid for SSRM in React
Let's walk through the basic setup in a React component.
1. Grid Options Configuration
First, you need to tell AG-Grid to use the Server-Side Row Model. This is done via the rowModelType property in your gridOptions or directly on the AgGridReact component.
import React, { useState, useRef, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
const MyServerSideGrid = () => {
const gridRef = useRef();
const [columnDefs] = useState([
{ field: 'id', headerName: 'ID', width: 90 },
{ field: 'athlete', headerName: 'Athlete', filter: true, sortable: true },
{ field: 'country', headerName: 'Country', filter: true, sortable: true },
{ field: 'year', headerName: 'Year', filter: 'agNumberColumnFilter', sortable: true },
{ field: 'sport', headerName: 'Sport', filter: true, sortable: true },
]);
const defaultColDef = {
flex: 1,
minWidth: 100,
resizable: true,
};
const getServerSideDatasource = useCallback(() => {
// This function will return our custom IServerSideDatasource implementation
// We'll define this next
return createMyServerSideDatasource();
}, []);
const onGridReady = useCallback((params) => {
params.api.setServerSideDatasource(getServerSideDatasource());
}, [getServerSideDatasource]);
return (
<div className="ag-theme-alpine" style={{ height: 700, width: '100%' }}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowModelType={'serverSide'} // <-- Important!
onGridReady={onGridReady}
serverSideStoreParams={{
// Optional: Configure how the SSRM fetches data
// e.g., cacheBlockSize, maxBlocksInCache
cacheBlockSize: 100, // Fetch 100 rows at a time
}}
/>
</div>
);
};
export default MyServerSideGrid;
2. Implementing IServerSideDatasource
Now, let's create the createMyServerSideDatasource function that returns an object conforming to IServerSideDatasource.
const createMyServerSideDatasource = () => {
return {
getRows: (params) => {
console.log('[Datasource] - Requesting rows:', params.request);
// In a real application, you would make an API call here.
// Example: axios.post('/api/data', params.request)
// For demonstration, let's simulate an API call
const url = 'http://localhost:3001/api/olympic-winners'; // Your actual API endpoint
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params.request),
})
.then(response => response.json())
.then(data => {
// data should contain 'rows' (the current block of data)
// and 'lastRow' (the total number of rows if known)
if (data.rows && typeof data.lastRow === 'number') {
params.successCallback(data.rows, data.lastRow);
} else {
console.error('Invalid data structure from server:', data);
params.failCallback();
}
})
.catch(error => {
console.error('Error fetching data:', error);
params.failCallback();
});
},
// init(params) {}, // Optional
// destroy() {}, // Optional
};
};
In this example, the getRows method constructs a POST request to a hypothetical server endpoint /api/olympic-winners, sending the full params.request object. The server is expected to respond with a JSON object containing rows and lastRow.
Server-Side Logic (Conceptual Example)
Your backend server needs to be able to parse the request sent by AG-Grid and return the appropriate data. Here's a conceptual example using Node.js with Express:
// server.js (Conceptual Node.js/Express Backend)
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors'); // For development, allow CORS
const app = express();
const PORT = 3001;
// Assume you have a large dataset, e.g., from a JSON file or database
// For simplicity, we'll use a local JSON file here
const allOlympicWinners = require('./olympicWinners.json');
app.use(cors());
app.use(bodyParser.json());
app.post('/api/olympic-winners', (req, res) => {
const request = req.body;
console.log('Server received request:', request);
let filteredData = [...allOlympicWinners];
// 1. Apply Filtering
if (request.filterModel) {
Object.keys(request.filterModel).forEach(key => {
const filter = request.filterModel[key];
if (filter.filterType === 'text') {
const filterValue = filter.filter.toLowerCase();
filteredData = filteredData.filter(row =>
row[key] && String(row[key]).toLowerCase().includes(filterValue)
);
}
// Add more filter types (number, date, etc.)
});
}
// 2. Apply Sorting
if (request.sortModel && request.sortModel.length > 0) {
request.sortModel.forEach(sortItem => {
filteredData.sort((a, b) => {
const aValue = a[sortItem.colId];
const bValue = b[sortItem.colId];
if (aValue === bValue) return 0;
if (sortItem.sort === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
});
}
// 3. Handle Pagination (startRow, endRow)
const totalRows = filteredData.length;
const rowsThisBlock = filteredData.slice(request.startRow, request.endRow);
res.json({
rows: rowsThisBlock,
lastRow: totalRows, // AG-Grid uses this to determine if more data exists
});
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
This server-side example demonstrates how to:
- Receive the AG-Grid request object.
- Apply basic text filtering based on
filterModel. - Apply sorting based on
sortModel. - Slice the data based on
startRowandendRowfor pagination. - Return the sliced data (`rows`) and the total count (`lastRow`).
For a real-world scenario, you would replace the in-memory array with database queries (SQL, NoSQL, etc.), dynamically building queries based on the AG-Grid request.
Advanced Features with SSRM
The Server-Side Row Model truly shines when combined with more advanced AG-Grid features:
- Server-Side Grouping: The grid can request grouped data from your server, which then performs the grouping and returns only the top-level groups or the expanded children.
- Server-Side Aggregation: Perform aggregations (sum, average, count) on the server across potentially millions of rows, returning only the aggregated values for display.
- Server-Side Pivoting: Transform data into pivot tables on the server before sending it to the client.
- Infinite Scrolling vs. Pagination: Configure
cacheBlockSizeandmaxBlocksInCachefor fine-grained control over how data is fetched and cached, enabling either traditional pagination or smooth infinite scrolling.
Each of these advanced features will augment the request object sent to your server, providing specific models (e.g., rowGroupCols, valueCols, pivotCols, pivotMode) that your backend must interpret and act upon.
Best Practices for SSRM
- Efficient Server Queries: Optimize your database queries to handle sorting, filtering, and pagination efficiently, especially with large datasets. Use database indexing heavily.
- Error Handling: Implement robust error handling on both the client (
failCallback) and server. - Loading Indicators: Provide visual feedback to users while data is being fetched (AG-Grid has built-in loading overlays).
- Debouncing Requests: For quick user interactions (like typing in a filter box), consider debouncing requests to the server to avoid flooding it with unnecessary calls.
- Cache Configuration: Tune
cacheBlockSizeand otherserverSideStoreParamsto balance between fewer network requests and lower client-side memory usage.