Row Data and State Management in AG-Grid with React
Welcome to the eighth installment of our AG-Grid React series! Today, we're diving deep into a fundamental aspect of building dynamic and responsive grid applications: managing row data and the overall state within AG-Grid. Understanding how to efficiently handle data and synchronize it with your React application's state is crucial for creating robust and performant grids.
AG-Grid is incredibly powerful because it abstracts away much of the complexity of rendering large datasets. However, as developers, we're responsible for providing that data and reacting to changes, both from the grid and from our application.
Understanding Row Data in AG-Grid
At its core, AG-Grid displays an array of JavaScript objects, where each object represents a row in the grid. This array is typically passed to the grid via the rowData prop. Each key-value pair in a row object corresponds to a potential column in your grid.
When you provide data to AG-Grid, it doesn't make a deep copy; it works with the reference you provide. This has important implications for how you update data, which we'll explore shortly.
Basic Row Data Management with React Local State
For many applications, especially those dealing with client-side data or smaller datasets, React's useState hook is perfectly adequate for managing your grid's row data.
Initial Data Loading
Let's start with a simple example of initializing AG-Grid with some data using useState.
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 MyGridComponent = () => {
const gridRef = useRef();
const [rowData, setRowData] = useState([
{ make: 'Toyota', model: 'Celica', price: 35000 },
{ make: 'Ford', model: 'Mondeo', price: 32000 },
{ make: 'Porsche', model: 'Boxster', price: 72000 },
]);
const [columnDefs] = useState([
{ field: 'make' },
{ field: 'model' },
{ field: 'price' },
]);
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
></AgGridReact>
</div>
);
};
export default MyGridComponent;
In this setup, any changes to the rowData state variable via setRowData will cause AG-Grid to re-render with the new data.
Mutating Row Data: Adding, Updating, and Deleting Rows
When you need to modify your row data, it's crucial to follow React's principles of immutability. This means instead of directly modifying the existing rowData array, you should create a new array with the desired changes and then pass that new array to setRowData. This allows React to detect changes efficiently and trigger the necessary re-renders.
Adding New Rows
To add a new row, you create a new array that includes all existing rows plus the new one.
// ... (imports and initial setup)
const MyGridComponent = () => {
// ... (rowData and columnDefs state)
const addRow = useCallback(() => {
const newRow = { make: 'Tesla', model: 'Model 3', price: 55000 + Math.floor(Math.random() * 1000) };
setRowData(prevData => [...prevData, newRow]);
}, []);
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<button onClick={addRow}>Add New Row</button>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
></AgGridReact>
</div>
);
};
Updating Existing Rows
To update a row, you'll typically find the row by a unique ID, create a new row object with the updated fields, and then replace the old row with the new one in a new array.
// ... (imports and initial setup, ensure rows have unique IDs)
const MyGridComponent = () => {
const [rowData, setRowData] = useState([
{ id: 'a', make: 'Toyota', model: 'Celica', price: 35000 },
{ id: 'b', make: 'Ford', model: 'Mondeo', price: 32000 },
{ id: 'c', make: 'Porsche', model: 'Boxster', price: 72000 },
]);
// ... (columnDefs state)
const updateRow = useCallback(() => {
const rowIdToUpdate = 'b';
const newPrice = 33500; // New price for Ford Mondeo
setRowData(prevData =>
prevData.map(row =>
row.id === rowIdToUpdate
? { ...row, price: newPrice } // Create a new object for the updated row
: row
)
);
}, []);
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<button onClick={updateRow}>Update Ford Mondeo Price</button>
{/* ... AgGridReact */}
</div>
);
};
Deleting Rows
Deleting rows involves filtering out the unwanted row(s) from the array to create a new one.
// ... (imports and initial setup, ensure rows have unique IDs)
const MyGridComponent = () => {
const [rowData, setRowData] = useState([
{ id: 'a', make: 'Toyota', model: 'Celica', price: 35000 },
{ id: 'b', make: 'Ford', model: 'Mondeo', price: 32000 },
{ id: 'c', make: 'Porsche', model: 'Boxster', price: 72000 },
]);
// ... (columnDefs state)
const deleteRow = useCallback(() => {
const rowIdToDelete = 'a'; // Delete Toyota Celica
setRowData(prevData =>
prevData.filter(row => row.id !== rowIdToDelete)
);
}, []);
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<button onClick={deleteRow}>Delete Toyota Celica</button>
{/* ... AgGridReact */}
</div>
);
};
Integrating with External State Management (Redux, Context API, Zustand, etc.)
For larger applications, or when data needs to be shared across many components, you might opt for a more centralized state management solution like Redux, the React Context API, Zustand, or Jotai.
The principle remains the same: your chosen state management solution will hold the "source of truth" for your row data. When this data changes, you'll dispatch actions or update the context/store, and your AG-Grid component will subscribe to these changes. The updated data will then be passed to the rowData prop, just as it would with local useState.
// Example using a hypothetical global store (e.g., Redux selector)
import React from 'react';
import { AgGridReact } from 'ag-grid-react';
// import { useSelector, useDispatch } from 'react-redux'; // If using Redux
const MyGridComponentWithExternalState = () => {
// const rowData = useSelector(state => state.gridData.items); // Get data from Redux store
// const dispatch = useDispatch();
// For demonstration, let's mock it:
const rowData = [
{ id: 'x', make: 'Honda', model: 'CRV', price: 28000 },
{ id: 'y', make: 'Nissan', model: 'Rogue', price: 29500 },
];
const [columnDefs] = React.useState([
{ field: 'make' },
{ field: 'model' },
{ field: 'price' },
]);
// Example of an action to update data via external state management
// const handleAddRow = () => {
// const newRow = { id: 'z', make: 'Mazda', model: 'CX-5', price: 30000 };
// dispatch({ type: 'ADD_GRID_ROW', payload: newRow });
// };
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
{/* <button onClick={handleAddRow}>Add Row via Store</button> */}
<AgGridReact
rowData={rowData} // Data comes from your external store
columnDefs={columnDefs}
></AgGridReact>
</div>>
);
};
AG-Grid's Internal State vs. External Data State
It's crucial to distinguish between two types of state:
-
Your Application's Data State (External State): This is the actual array of row data (
rowDataprop) you provide to the grid. It's managed by React'suseState, your Redux store, etc. -
AG-Grid's Internal UI State: This includes things like:
- Column sorting order
- Column filtering criteria
- Column visibility and order
- Row selection
- Cell editing state
- Scroll position
When you update the rowData prop, AG-Grid will re-render its rows. However, its internal UI state (like applied filters or sorts) often persists across these data updates. This is usually desired behavior.
Accessing and Controlling Internal State via Grid API
You can programmatically interact with AG-Grid's internal state using the Grid API. You get access to the API via the onGridReady callback or by using a ref.
// ... (imports and setup)
const MyGridComponent = () => {
const gridRef = useRef();
const [rowData, setRowData] = useState(/* ... */);
const [columnDefs] = useState(/* ... */);
const onGridReady = useCallback((params) => {
// You can store params.api and params.columnApi if needed
// For simplicity, we'll use gridRef.current.api directly later
}, []);
const getSelectedRows = useCallback(() => {
const selectedNodes = gridRef.current.api.getSelectedNodes();
const selectedData = selectedNodes.map(node => node.data);
console.log('Selected Rows:', selectedData);
}, []);
const applyFilter = useCallback(() => {
const filterModel = {
make: {
type: 'equals',
filter: 'Ford',
},
};
gridRef.current.api.setFilterModel(filterModel);
gridRef.current.api.onFilterChanged(); // Notify grid that filter changed
}, []);
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<button onClick={getSelectedRows}>Log Selected Rows</button>
<button onClick={applyFilter}>Filter by Ford</button>
<AgGridReact
ref={gridRef}
onGridReady={onGridReady}
rowData={rowData}
columnDefs={columnDefs}
rowSelection="multiple" // Enable row selection
></AgGridReact>
</div>
);
};
The Importance of getRowNodeId for Stable IDs
When your rowData changes frequently (e.g., adding/removing rows, reordering, or refreshing data from a server), AG-Grid needs a way to uniquely identify each row to maintain its internal state correctly. This is where the getRowNodeId prop comes in.
By providing a function to getRowNodeId that returns a unique identifier for each data item, AG-Grid can:
- Preserve row selection when data is updated.
- Maintain row expansion state.
- Keep cell editing state even if the underlying data array reference changes.
- Ensure smooth animations for row additions/deletions.
Always use getRowNodeId if your row data has a stable, unique identifier.
// ... (imports and setup)
const MyGridComponent = () => {
const [rowData, setRowData] = useState([
{ id: 'a', make: 'Toyota', model: 'Celica', price: 35000 },
{ id: 'b', make: 'Ford', model: 'Mondeo', price: 32000 },
{ id: 'c', make: 'Porsche', model: 'Boxster', price: 72000 },
]);
// ... (columnDefs state)
const getRowNodeId = useCallback((data) => data.id, []); // Use 'id' as unique identifier
// ... (rest of component, e.g., buttons to add/update rows)
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
getRowNodeId={getRowNodeId} // Pass the function here
></AgGridReact>
</div>
);
};
Without getRowNodeId, if you update the entire rowData array, AG-Grid might treat all rows as new, potentially losing selection or other transient states.
Server-Side Row Data and State (Brief Mention)
For very large datasets (tens of thousands or millions of rows), fetching all data at once and managing it client-side becomes inefficient. AG-Grid offers server-side row models (SSRM, ISRM) designed for this scenario.
In these advanced models, you typically don't pass data directly via the rowData prop. Instead, AG-Grid requests data from your server as the user scrolls, sorts, filters, or groups. Your application's state management then focuses on providing the necessary parameters for these server-side calls and handling the returned chunks of data. This is a more complex topic that warrants its own dedicated discussion.
Best Practices for AG-Grid State Management
-
Immutability is Key: Always create new arrays and objects when updating
rowDataor individual row items to ensure React and AG-Grid detect changes efficiently. -
Use
getRowNodeId: Implement this prop if your data items have stable, unique IDs to preserve grid state across data updates. - Separate Concerns: Keep your data fetching and manipulation logic distinct from your AG-Grid configuration.
-
Leverage Grid API: For interacting with AG-Grid's internal UI state (sorting, filtering, selection), use the Grid API methods rather than trying to manipulate
rowDatadirectly for these purposes. - Performance Considerations: For very large client-side datasets, consider techniques like virtualization (which AG-Grid handles internally) and memoization for complex selectors or derived data.
-
Choose Appropriate State Management: Use
useStatefor simple, contained data, and consider external solutions for shared, complex, or global state.
Conclusion
Effective row data and state management are foundational to building high-performance and user-friendly AG-Grid applications in React. By understanding the distinction between your application's data state and AG-Grid's internal UI state, adhering to immutability principles, and leveraging powerful features like getRowNodeId and the Grid API, you can confidently manage complex data flows and create truly dynamic grids.
In our next installment, we'll likely explore cell rendering or editing, further building upon our data management knowledge. Stay tuned!