Infinite Scrolling in AG-Grid with React: Mastering Large Datasets
Managing and displaying vast amounts of data efficiently is a common challenge in modern web applications. Traditional methods of loading all data at once can lead to significant performance bottlenecks, slow page loads, and a poor user experience. This is where infinite scrolling, also known as virtual scrolling or lazy loading, becomes invaluable.
In this installment of our AG-Grid React series, we'll dive deep into implementing infinite scrolling, leveraging AG-Grid's powerful Infinite Row Model. This approach ensures your application remains responsive and performant, even when dealing with millions of records, by only fetching and rendering the data currently visible to the user.
Understanding AG-Grid Row Models
Before we jump into infinite scrolling, it's crucial to understand AG-Grid's concept of Row Models. A Row Model is responsible for managing the rows displayed in the grid. AG-Grid offers several built-in row models:
- Client-Side Row Model: The default and simplest model, suitable for small to medium datasets where all data can be loaded into the browser memory.
- Server-Side Row Model: Designed for very large datasets where filtering, sorting, and grouping are performed on the server.
- Viewport Row Model: Ideal for specific scenarios where the server pushes updates to the grid.
- Infinite Row Model: Our focus for today. It's designed for displaying large datasets without loading all data into the client, fetching rows in blocks as the user scrolls.
The Infinite Row Model: How It Works
The Infinite Row Model works by requesting blocks of rows from a Datasource as the user scrolls. Instead of sending the entire dataset to the browser, the grid asks the datasource for a specific range of rows (e.g., rows 0-99, then 100-199, and so on). This significantly reduces initial load times and memory footprint.
Key Characteristics:
- Lazy Loading: Data is fetched only when needed.
- Block-Based Requests: Data is requested in configurable blocks (e.g., 100 rows at a time).
- Server-Side Pagination: Effectively implements server-side pagination transparently to the user.
- Limited Client-Side State: Only a small cache of visible rows is maintained in the browser.
Setting Up Infinite Scrolling in AG-Grid React
Implementing infinite scrolling involves two primary steps:
- Configuring the AG-Grid instance to use the Infinite Row Model.
- Providing a Datasource that knows how to fetch data in blocks.
1. Grid Configuration (gridOptions)
The first step is to tell AG-Grid to use the Infinite Row Model. This is done through the gridOptions.
import React, { useState, useRef, useCallback, useEffect } 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 InfiniteScrollingGrid = () => {
const gridRef = useRef();
const [columnDefs, setColumnDefs] = useState([
{ field: 'id', headerName: 'ID', width: 90 },
{ field: 'make', headerName: 'Make', width: 150 },
{ field: 'model', headerName: 'Model', width: 150 },
{ field: 'price', headerName: 'Price', width: 100 }
]);
// Grid options for Infinite Row Model
const defaultGridOptions = {
rowModelType: 'infinite', // Crucial setting for infinite scrolling
pagination: false, // Pagination is handled by the infinite scroll
cacheBlockSize: 100, // Number of rows fetched per block
maxBlocksInCache: 10, // Max number of blocks to keep in memory
infiniteInitialRowCount: 1, // Minimum count for the grid to render initial rows
getRowId: useCallback((params) => params.data.id, []), // Essential for updates/selection
};
const onGridReady = useCallback((params) => {
// Instantiate the datasource when the grid is ready
const datasource = createMyDatasource();
params.api.setDatasource(datasource);
}, []);
// ... (rest of the component)
return (
<div className="ag-theme-alpine" style={{ height: 500, width: '100%' }}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
gridOptions={defaultGridOptions}
onGridReady={onGridReady}
></AgGridReact>
</div>
);
};
export default InfiniteScrollingGrid;
Let's break down the key gridOptions properties:
rowModelType: 'infinite': This is the most important setting, telling AG-Grid to use the Infinite Row Model.cacheBlockSize: 100: Defines how many rows AG-Grid will request in each block. Adjust this based on your network latency and data size. Larger blocks mean fewer requests but more data per request.maxBlocksInCache: 10: Controls how many blocks (not rows) the grid will keep in its cache. If the user scrolls outside this range, older blocks are evicted, and new ones are loaded.infiniteInitialRowCount: 1: This tells the grid to initially assume there's at least one row. The actual total row count will be provided by the datasource.getRowId: (params) => params.data.id: Crucial for performance and correctness. This function provides a unique identifier for each row, which AG-Grid uses for tracking row state (e.g., selection, expanded rows) across different data loads.
2. Implementing the Datasource
The Datasource is an object that implements the IDatasource interface, which has a single method: getRows(params). This method is called by AG-Grid whenever it needs more data.
const createMyDatasource = () => {
// Simulate a backend API call
const fetchData = (startRow, endRow) => {
return new Promise((resolve) => {
// Simulate network delay
setTimeout(() => {
const allData = Array.from({ length: 10000 }, (_, i) => ({ // Simulate 10,000 total rows
id: i,
make: ['Toyota', 'Ford', 'Porsche'][i % 3],
model: ['Celica', 'Mondeo', 'Boxster', 'GT86'][i % 4],
price: 35000 + (i * 10)
}));
const rowsThisPage = allData.slice(startRow, endRow);
const lastRow = Math.min(endRow, allData.length);
resolve({
rowsThisPage: rowsThisPage,
totalRowCount: allData.length // Always send the total row count
});
}, 500); // 500ms delay to simulate API call
});
};
return {
getRows: (params) => {
console.log('Asking for rows: ' + params.startRow + ' to ' + params.endRow);
fetchData(params.startRow, params.endRow)
.then(response => {
// Call the success callback with the fetched rows and total row count
params.successCallback(response.rowsThisPage, response.totalRowCount);
})
.catch(error => {
console.error("Error fetching data:", error);
// Call the fail callback on error
params.failCallback();
});
},
};
};
Let's examine the getRows method:
params.startRow: The starting row index requested by the grid.params.endRow: The ending row index requested by the grid (exclusive).params.successCallback(rowsThisPage, totalRowCount): This is what you call once your data is fetched successfully.rowsThisPage: An array of row data for the requested block.totalRowCount: The *total* number of rows available on the server. This is crucial for AG-Grid to correctly display the scrollbar and know when to stop requesting more data. If you don't know the total, passnullorundefined, and the grid will assume it needs to keep scrolling indefinitely until no more rows are returned.
params.failCallback(): Call this if there's an error fetching data.
In a real-world scenario, fetchData would make an actual API call to your backend, passing startRow and endRow (or page number and page size) as parameters, and receiving a paginated response.
Putting It All Together (Full Example Component)
Here's the complete React component combining the grid setup and the datasource:
import React, { useState, useRef, useCallback, useEffect } 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 InfiniteScrollingGrid = () => {
const gridRef = useRef();
const [columnDefs, setColumnDefs] = useState([
{ field: 'id', headerName: 'ID', width: 90 },
{ field: 'make', headerName: 'Make', width: 150 },
{ field: 'model', headerName: 'Model', width: 150 },
{ field: 'price', headerName: 'Price', width: 100 }
]);
const defaultGridOptions = {
rowModelType: 'infinite',
pagination: false,
cacheBlockSize: 100,
maxBlocksInCache: 10,
infiniteInitialRowCount: 1,
getRowId: useCallback((params) => params.data.id, []),
// Optional: Show loading overlay while data is being fetched
overlayLoadingTemplate: '<span class="ag-overlay-loading-center">Please wait while your data is loading...</span>',
};
const createMyDatasource = () => {
const fetchData = (startRow, endRow) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulate a large dataset of 10,000 rows
const allData = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1, // Start IDs from 1
make: ['Toyota', 'Ford', 'Porsche', 'BMW', 'Mercedes'][i % 5],
model: ['Celica', 'Mondeo', 'Boxster', 'M3', 'C-Class', 'Mustang', 'Camry'][i % 7],
price: 25000 + (i * 50)
}));
const rowsThisPage = allData.slice(startRow, endRow);
const totalRowCount = allData.length; // Actual total rows
// Simulate potential errors for demonstration
if (Math.random() < 0.05) { // 5% chance of failure
reject("Failed to fetch data!");
} else {
resolve({
rowsThisPage: rowsThisPage,
totalRowCount: totalRowCount
});
}
}, 500); // Simulate network delay
});
};
return {
getRows: (params) => {
console.log('AG-Grid requesting rows: ' + params.startRow + ' to ' + params.endRow);
// Show loading overlay
params.api.showLoadingOverlay();
fetchData(params.startRow, params.endRow)
.then(response => {
params.api.hideOverlay(); // Hide loading overlay
params.successCallback(response.rowsThisPage, response.totalRowCount);
})
.catch(error => {
console.error("Error fetching data:", error);
params.api.showNoRowsOverlay(); // Show error/no rows overlay
params.failCallback();
});
},
};
};
const onGridReady = useCallback((params) => {
const datasource = createMyDatasource();
params.api.setDatasource(datasource);
}, []);
return (
<div style={{ width: '100%', height: 'calc(100vh - 50px)' }} className="ag-theme-alpine">
<h2>AG-Grid Infinite Scrolling Example</h2>
<p>Scroll down to load more data. Simulating 10,000 rows, loading in blocks of 100.</p>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
gridOptions={defaultGridOptions}
onGridReady={onGridReady}
></AgGridReact>
</div>
);
};
export default InfiniteScrollingGrid;
Key Considerations and Best Practices
1. Backend Integration
The `createMyDatasource` function simulates a backend API. In a real application, you would replace the `fetchData` function with actual AJAX calls (e.g., using `axios` or the built-in `fetch` API) to your server endpoint. Your server would need to accept `startRow` and `endRow` parameters and return a paginated slice of data along with the total count.
// Example of how fetchData might look with a real API
const fetchData = async (startRow, endRow, sortModel, filterModel) => {
try {
const response = await fetch(`/api/data?startRow=${startRow}&endRow=${endRow}&sort=${JSON.stringify(sortModel)}&filter=${JSON.stringify(filterModel)}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
return {
rowsThisPage: data.rows,
totalRowCount: data.total
};
} catch (error) {
console.error("API call failed:", error);
throw error; // Re-throw to be caught by the datasource's .catch()
}
};
2. Sorting and Filtering
For sorting and filtering to work with the Infinite Row Model, these operations must be handled on the server. When a sort or filter is applied by the user, AG-Grid will call `getRows` again, but this time the `params` object will also contain `params.sortModel` and `params.filterModel`. You need to pass these to your backend API, and the backend should apply the sorting/filtering before returning the paginated data.
3. Loading Indicators and Error Handling
It's good practice to provide feedback to the user when data is being fetched. AG-Grid offers built-in overlay templates:
- `overlayLoadingTemplate`: Shown when data is loading.
- `overlayNoRowsTemplate`: Shown when no rows are available or on `failCallback`.
4. `getRowId` Importance
Defining `getRowId` is crucial. Without it, if a block of data is reloaded (e.g., after a sort or filter, or if it was evicted from cache and scrolled back into view), AG-Grid cannot maintain the state of individual rows (like selection). Ensure your unique ID is stable and present in your data.
Conclusion
The AG-Grid Infinite Row Model is a powerful solution for displaying large datasets in your React applications without compromising performance or user experience. By implementing a custom datasource and configuring the grid correctly, you can achieve seamless, lazy-loaded data presentation that scales to millions of records. Remember to handle sorting, filtering, and error states gracefully by integrating these concerns with your backend API.
With this setup, your AG-Grid tables will remain fast, responsive, and a pleasure to use, regardless of the data volume you throw at them.