AG-Grid React Series #20: Master-Detail Grid in React
In many enterprise applications, data is often hierarchical or contextual. Displaying all related information in a single flat grid can lead to an overwhelming user experience. This is where the Master-Detail pattern shines, allowing users to expand a "master" row to reveal related "detail" information, often presented in another grid. AG-Grid offers a powerful and flexible way to implement this pattern in your React applications.
This installment of our AG-Grid React series will guide you through building a Master-Detail grid, where clicking an expand icon on a master row unveils a secondary, fully functional AG-Grid containing the detail data.
Understanding AG-Grid's Master-Detail Concept
At its core, AG-Grid's Master-Detail feature allows you to render custom content within a detail row, which is shown when a master row is expanded. While this custom content can be anything, a common and very effective use case is to embed another AG-Grid instance – creating a nested grid experience.
Key components for implementing Master-Detail are:
- Master Grid: The primary grid displaying high-level data.
- Detail Cell Renderer: A custom React component responsible for rendering the content within the expanded detail row. This is where our "detail" AG-Grid will live.
- Detail Grid: The secondary grid embedded within the detail cell renderer, displaying the specific data related to its parent master row.
Prerequisites
Before we dive into the code, ensure you have a basic React project set up with AG-Grid installed. If not, you can quickly add it:
npm install ag-grid-community ag-grid-react
# or
yarn add ag-grid-community ag-grid-react
Step-by-Step Implementation
1. Define Your Data Structure
For Master-Detail to work effectively, your master grid's rowData should contain the detail data associated with each master row. A common pattern is to have an array property on each master row object that holds its corresponding detail items.
const masterRowData = [
{
id: 'C001',
customer: 'Acme Corp',
totalOrders: 5,
details: [
{ orderId: 'ORD001', item: 'Laptop', quantity: 1, price: 1200 },
{ orderId: 'ORD002', item: 'Mouse', quantity: 2, price: 25 },
],
},
{
id: 'C002',
customer: 'Globex Inc',
totalOrders: 3,
details: [
{ orderId: 'ORD003', item: 'Keyboard', quantity: 1, price: 75 },
{ orderId: 'ORD004', item: 'Monitor', quantity: 1, price: 300 },
{ orderId: 'ORD005', item: 'Webcam', quantity: 1, price: 50 },
],
},
// ... more master rows
];
2. Configure the Master Grid
The master grid needs to be told that it's a master-detail grid and how to render its detail rows.
- Set
masterDetail: trueon the master grid options. - Define a
detailCellRendererproperty, pointing to our custom React component that will render the detail grid. - Use
detailCellRendererParamsto pass any configuration or data to the detail renderer. - One of your master
columnDefsneedscellRenderer: 'agGroupCellRenderer'to display the expand/collapse icon.
// App.js or MasterGridComponent.js
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';
import { DetailCellRenderer } from './DetailCellRenderer'; // We'll create this next
const MasterGridExample = () => {
const gridRef = useRef();
const [rowData] = useState([
{
id: 'C001',
customer: 'Acme Corp',
totalOrders: 5,
details: [
{ orderId: 'ORD001', item: 'Laptop', quantity: 1, price: 1200 },
{ orderId: 'ORD002', item: 'Mouse', quantity: 2, price: 25 },
],
},
{
id: 'C002',
customer: 'Globex Inc',
totalOrders: 3,
details: [
{ orderId: 'ORD003', item: 'Keyboard', quantity: 1, price: 75 },
{ orderId: 'ORD004', item: 'Monitor', quantity: 1, price: 300 },
{ orderId: 'ORD005', item: 'Webcam', quantity: 1, price: 50 },
],
},
{
id: 'C003',
customer: 'Initech',
totalOrders: 2,
details: [
{ orderId: 'ORD006', item: 'Server Rack', quantity: 1, price: 800 },
{ orderId: 'ORD007', item: 'Cable Ties', quantity: 5, price: 5 },
],
},
]);
const [masterColumnDefs] = useState([
{
field: 'customer',
cellRenderer: 'agGroupCellRenderer', // This column will show the expand/collapse icon
minWidth: 200
},
{ field: 'id', headerName: 'Customer ID', maxWidth: 120 },
{ field: 'totalOrders', headerName: 'Total Orders', maxWidth: 150 },
]);
const detailGridOptions = {
columnDefs: [ // Column definitions for the detail grid
{ field: 'orderId', headerName: 'Order ID', maxWidth: 120 },
{ field: 'item', minWidth: 150 },
{ field: 'quantity', maxWidth: 100 },
{ field: 'price', valueFormatter: p => '$' + p.value.toFixed(2) },
],
defaultColDef: {
flex: 1,
resizable: true,
},
// You can add more detail grid specific options here
};
const defaultColDef = {
flex: 1,
resizable: true,
};
return (
Master Customers
{
setTimeout(() => {
params.api.getDisplayedRowAtIndex(0).setExpanded(true);
}, 0);
}, [])}
/>
);
};
export default MasterGridExample;
3. Create the Detail Cell Renderer Component
This React component will receive props from the master grid, including the rowData for its corresponding master row. It will then render an AgGridReact instance using the detail data extracted from these props.
// DetailCellRenderer.js
import React, { memo, useEffect, useRef, useState, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
export const DetailCellRenderer = memo((props) => {
const detailGridRef = useRef(null);
const [detailRowData, setDetailRowData] = useState([]);
// Extract detail row data from the master row's 'details' property
useEffect(() => {
if (props.data && props.data.details) {
setDetailRowData(props.data.details);
}
}, [props.data]);
// Handle grid ready for the detail grid
const onGridReady = useCallback((params) => {
// You can programmatically interact with the detail grid here
// e.g., auto-size columns, apply filters, etc.
console.log('Detail Grid Ready:', params.api);
}, []);
// Get the column definitions for the detail grid from master's detailCellRendererParams
const detailColumnDefs = props.detailGridOptions.columnDefs || [];
const detailDefaultColDef = props.detailGridOptions.defaultColDef || {};
return (
<div className="full-width-panel">
<div className="full-width-details">
<p>
<strong>Order Details for {props.data.customer}</strong>
</p>
</div>
<div
style={{ height: '200px', width: '100%' }} // Give the detail grid a fixed height
className="ag-theme-alpine"
>
<AgGridReact
ref={detailGridRef}
rowData={detailRowData}
columnDefs={detailColumnDefs}
defaultColDef={detailDefaultColDef}
onGridReady={onGridReady}
// Optional: enable pagination for detail grid if many items
// pagination={true}
// paginationPageSize={5}
></AgGridReact>
</div>
</div>
);
});
Key Considerations & Best Practices
Performance with Large Datasets
While AG-Grid is highly performant, rendering many detail grids, each potentially with many rows, can impact performance.
-
Lazy Loading: For very large detail datasets, consider fetching detail data from an API only when a master row is expanded, rather than loading all detail data upfront. You would update the
detailRowDatastate within yourDetailCellRendererafter making an API call. -
Detail Grid Optimization: Ensure your detail grids use appropriate
defaultColDef, enable virtualization, and use features like pagination if they contain many rows.
Customizing Detail Grids
Each detail grid is a fully independent AG-Grid instance. This means you can apply all standard AG-Grid features to them:
- Sorting, Filtering, Grouping
- Custom Cell Renderers/Editors
- Row Selection
- Themes and Styling
- Context Menus
You can pass specific configurations for each detail grid via the detailCellRendererParams.detailGridOptions as shown in the example.
Interactions and Communication
Sometimes, you might need to communicate between the master grid and its detail grids, or between different detail grids.
- Master to Detail: Data flows naturally via props. You can pass callbacks or API instances from the master to the detail renderer if needed.
-
Detail to Master: The detail grid component can emit events or call functions passed down from the master grid (via
detailCellRendererParams) to inform the master of changes.
Dynamic Detail Data
If your detail data isn't immediately available with the master row data (e.g., it needs to be fetched from an API), you can modify your DetailCellRenderer to handle this.
// Inside DetailCellRenderer.js (example for dynamic fetch)
useEffect(() => {
const fetchDetailData = async () => {
if (props.data && props.data.id) {
// Simulate API call
const response = await new Promise(resolve => setTimeout(() => {
const fetchedData = [
{ orderId: 'DYN001', item: 'Remote Item A', quantity: 1, price: 99 },
{ orderId: 'DYN002', item: 'Remote Item B', quantity: 3, price: 15 },
];
resolve(fetchedData);
}, 500));
setDetailRowData(response);
}
};
fetchDetailData();
}, [props.data]); // Re-fetch if master row data changes
Conclusion
The Master-Detail feature in AG-Grid for React provides an incredibly powerful way to present complex, hierarchical data in an intuitive and user-friendly manner. By leveraging custom React components as detail cell renderers, you gain full control over the detailed view, allowing you to embed fully functional AG-Grid instances or any other custom UI. This flexibility makes AG-Grid an excellent choice for building sophisticated data-driven applications.
Experiment with the provided examples, explore the extensive AG-Grid documentation for more advanced configurations, and unlock the full potential of nested grids in your React projects!