AG-Grid-React Series #32: Programmatic Grid Control in React
AG-Grid is a powerful data grid for enterprise applications, and when integrated with React, it offers unparalleled flexibility. While users can interact with the grid directly through its UI, many advanced scenarios demand the ability to control the grid's behavior programmatically. This post, part of our AG-Grid-React series, dives deep into how you can take the reins of your AG-Grid instance directly from your React components, opening up a world of possibilities for dynamic data manipulation and enhanced user experiences.
Why Programmatic Grid Control?
Programmatic control allows your React application to interact with and modify the AG-Grid instance without relying on user interface actions. This capability is crucial for:
- External Triggers: Implementing buttons or logic outside the grid to perform actions like "Select All Rows," "Export Data," or "Reset Filters."
- Complex Workflows: Guiding users through multi-step processes where grid state needs to be updated based on external application logic.
- Dynamic Updates: Automatically adjusting column visibility, width, or data based on user roles, real-time data changes, or application settings.
- Automation and Testing: Scripting interactions for automated tests or batch operations.
Accessing the Grid's APIs: `gridApi` and `columnApi`
At the heart of programmatic control are two essential objects provided by AG-Grid:
-
gridApi: This object provides methods to interact with the grid's overall state, rows, data, and general functions (e.g., selecting rows, updating data, exporting). -
columnApi: This object focuses specifically on column-related operations (e.g., changing column visibility, width, applying column state).
You can access these APIs in a few ways, but the most common and recommended approach in modern React is using React's useRef hook.
Using `useRef` for API Access
The useRef hook allows you to create mutable objects that persist across renders without causing re-renders when their content changes. It's perfect for holding references to DOM nodes or, in this case, the AG-Grid API objects.
Here's how to set it up:
import React, { useRef, useState, 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(); // Reference to the AG-Grid component
const [rowData] = 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' }
]);
const onGridReady = useCallback((params) => {
// You can also store the APIs directly here if preferred,
// but useRef on the component itself is generally cleaner for external access.
// params.api.sizeColumnsToFit();
}, []);
const selectAllRows = () => {
if (gridRef.current && gridRef.current.api) {
gridRef.current.api.selectAll();
}
};
return (
<div style={{ width: '100%', height: '300px' }} className="ag-theme-alpine">
<button onClick={selectAllRows}>Select All Rows</button>
<AgGridReact
ref={gridRef} // Assign the ref to the AgGridReact component
rowData={rowData}
columnDefs={columnDefs}
onGridReady={onGridReady}
></AgGridReact>
</div>
);
};
export default MyGridComponent;
In this setup, `gridRef.current.api` will give you access to the gridApi, and `gridRef.current.columnApi` will give you access to the columnApi once the grid is ready.
Common Programmatic Operations
Let's explore some frequently used methods available on both gridApi and columnApi.
Row Operations (via `gridApi`)
-
selectAll()/deselectAll(): Selects or deselects all rows in the grid.gridRef.current.api.selectAll(); gridRef.current.api.deselectAll(); -
getSelectedRows(): Returns an array of data objects for all selected rows.const selectedData = gridRef.current.api.getSelectedRows(); console.log('Selected Rows:', selectedData); -
updateRowData(transaction): Adds, removes, or updates rows efficiently. Thetransactionobject containsadd,remove, andupdatearrays.const newRow = { make: 'Tesla', model: 'Model 3', price: 50000 }; gridRef.current.api.updateRowData({ add: [newRow] }); const rowToRemove = selectedData[0]; // Assuming you have selected rows gridRef.current.api.updateRowData({ remove: [rowToRemove] }); const updatedRow = { ...selectedData[0], price: 55000 }; gridRef.current.api.updateRowData({ update: [updatedRow] }); -
setRowData(rowData): Replaces all existing row data with the new array. Use with caution for large datasets as it can be less performant than `updateRowData`.const newDataSet = [ /* ... new array of data ... */ ]; gridRef.current.api.setRowData(newDataSet); -
ensureIndexVisible(index)/ensureNodeVisible(node): Scrolls the grid to ensure a specific row index or node is visible.gridRef.current.api.ensureIndexVisible(5); // Scroll to row with index 5
Column Operations (via `columnApi`)
-
setColumnVisible(key, visible): Hides or shows a column based on its field or ID.gridRef.current.columnApi.setColumnVisible('price', false); // Hide the 'price' column gridRef.current.columnApi.setColumnVisible('price', true); // Show the 'price' column -
setColumnWidth(key, newWidth, finished?): Sets a specific width for a column.gridRef.current.columnApi.setColumnWidth('make', 200); -
resetColumnState(): Resets all columns to their initial state as defined in `columnDefs`.gridRef.current.columnApi.resetColumnState(); -
applyColumnState(params): Applies a specific column state (e.g., order, visibility, width). Useful for saving and restoring user preferences.// Example: Saving state const currentState = gridRef.current.columnApi.getColumnState(); localStorage.setItem('gridColumnState', JSON.stringify(currentState)); // Example: Restoring state const savedState = JSON.parse(localStorage.getItem('gridColumnState')); if (savedState) { gridRef.current.columnApi.applyColumnState({ state: savedState, applyOrder: true }); }
Grid State & Utility Operations (via `gridApi`)
-
sizeColumnsToFit(): Resizes all columns to fit the grid's width.gridRef.current.api.sizeColumnsToFit(); -
exportDataAsCsv(params?): Exports the current grid data to a CSV file.gridRef.current.api.exportDataAsCsv({ fileName: 'my-grid-data.csv', skipHeader: false, }); -
setFilterModel(model)/getFilterModel(): Programmatically applies or retrieves the current filter model.// Apply a filter gridRef.current.api.setFilterModel({ make: { type: 'set', values: ['Toyota', 'Ford'] } }); // Get current filter model const currentFilterModel = gridRef.current.api.getFilterModel(); console.log('Current Filters:', currentFilterModel); -
setSortModel(model)/getSortModel(): Programmatically applies or retrieves the current sort model.// Apply a sort gridRef.current.api.setSortModel([ { colId: 'make', sort: 'asc' }, { colId: 'price', sort: 'desc' } ]); // Get current sort model const currentSortModel = gridRef.current.api.getSortModel(); console.log('Current Sort:', currentSortModel);
A Comprehensive Example: External Grid Controls
Let's combine some of these concepts into a single React component that provides external buttons to control the grid.
import React, { useRef, useState, 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 DynamicGridControl = () => {
const gridRef = useRef(); // Ref for the grid instance
const [rowData, setRowData] = useState([
{ id: 1, make: 'Toyota', model: 'Celica', price: 35000, age: 10 },
{ id: 2, make: 'Ford', model: 'Mondeo', price: 32000, age: 5 },
{ id: 3, make: 'Porsche', model: 'Boxster', price: 72000, age: 2 },
{ id: 4, make: 'Tesla', model: 'Model Y', price: 60000, age: 1 },
{ id: 5, make: 'BMW', model: 'X5', price: 65000, age: 7 },
]);
const [columnDefs] = useState([
{ field: 'id', headerName: 'ID', width: 80 },
{ field: 'make', headerName: 'Make', sortable: true, filter: true },
{ field: 'model', headerName: 'Model', sortable: true, filter: true },
{ field: 'price', headerName: 'Price', sortable: true, filter: true },
{ field: 'age', headerName: 'Age', sortable: true, filter: true },
]);
const onGridReady = useCallback(() => {
// You might perform initial setup here, e.g., sizeColumnsToFit
// gridRef.current.api.sizeColumnsToFit();
}, []);
const handleSelectAll = () => {
gridRef.current.api.selectAll();
};
const handleDeselectAll = () => {
gridRef.current.api.deselectAll();
};
const handleGetSelectedRows = () => {
const selectedNodes = gridRef.current.api.getSelectedNodes();
const selectedData = selectedNodes.map(node => node.data);
alert(`Selected Data: ${JSON.stringify(selectedData.map(d => d.make))}`);
};
const handleToggleAgeColumn = () => {
const isAgeVisible = gridRef.current.columnApi.getColumn('age').isVisible();
gridRef.current.columnApi.setColumnVisible('age', !isAgeVisible);
};
const handleAddRow = () => {
const newId = Math.max(...rowData.map(r => r.id)) + 1;
const newCar = { id: newId, make: 'New Make', model: 'New Model', price: 40000 + newId * 100, age: Math.floor(Math.random() * 10) + 1 };
gridRef.current.api.applyTransaction({ add: [newCar] });
setRowData(prevData => [...prevData, newCar]); // Keep local state in sync
};
const handleExportCsv = () => {
gridRef.current.api.exportDataAsCsv({
fileName: 'cars_data.csv',
skipHeader: false,
columnKeys: ['make', 'model', 'price'] // Only export these columns
});
};
const handleApplyPriceFilter = () => {
gridRef.current.api.setFilterModel({
price: {
filterType: 'number',
type: 'greaterThan',
filter: 50000
}
});
gridRef.current.api.onFilterChanged(); // Notify grid that filters have changed
};
const handleClearFilters = () => {
gridRef.current.api.setFilterModel(null);
gridRef.current.api.onFilterChanged();
};
return (
<div style={{ width: '100%' }}>
<div style={{ marginBottom: '10px' }}>
<button onClick={handleSelectAll}>Select All</button>
<button onClick={handleDeselectAll} style={{ marginLeft: '5px' }}>Deselect All</button>
<button onClick={handleGetSelectedRows} style={{ marginLeft: '5px' }}>Get Selected</button>
<button onClick={handleToggleAgeColumn} style={{ marginLeft: '5px' }}>Toggle Age Column</button>
<button onClick={handleAddRow} style={{ marginLeft: '5px' }}>Add New Row</button>
<button onClick={handleExportCsv} style={{ marginLeft: '5px' }}>Export CSV</button>
<button onClick={handleApplyPriceFilter} style={{ marginLeft: '5px' }}>Filter > $50k</button>
<button onClick={handleClearFilters} style={{ marginLeft: '5px' }}>Clear Filters</button>
</div>
<div style={{ height: '350px' }} className="ag-theme-alpine">
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
onGridReady={onGridReady}
rowSelection="multiple"
></AgGridReact>
</div>
</div>
);
};
export default DynamicGridControl;
Best Practices and Considerations
- API Availability: Always ensure the `gridApi` and `columnApi` are available before calling methods on them. The `gridRef.current` object will only be populated after the component mounts and the grid initializes. Conditional checks like `if (gridRef.current && gridRef.current.api)` are good practice.
- Performance: While powerful, be mindful of the performance implications when performing frequent programmatic updates on very large datasets. Methods like `updateRowData` are optimized for incremental changes, while `setRowData` causes a full re-render of rows.
- State Management: When programmatically changing grid data, ensure your React component's local state (e.g., `rowData`) or external state management (Redux, Zustand) remains in sync with the grid's internal state if you need to reflect those changes elsewhere in your application. Using `applyTransaction` and then updating local state, as shown in the example, is a robust pattern.
- Event Handling: Remember that programmatic changes might not always trigger the same grid events as user interactions. If your application relies on specific grid events, you might need to trigger related logic manually or carefully choose API methods that do trigger them (e.g., `onFilterChanged()` after `setFilterModel()`).
Conclusion
Mastering programmatic grid control with AG-Grid in React is a significant step towards building highly interactive and sophisticated data applications. By leveraging the gridApi and columnApi, you can empower your users with dynamic controls, automate complex workflows, and seamlessly integrate AG-Grid into the broader logic of your React application. Experiment with these powerful APIs to unlock the full potential of your AG-Grid implementations.