Using AG-Grid with Redux for Centralized State Management
Welcome to Series #52 of our AG-Grid-React journey! In modern React applications, managing complex state can quickly become challenging. This is especially true when dealing with data-intensive components like AG-Grid, where you might need to manage row data, column definitions, grid options, and potentially even user interactions (like filtering or sorting state) across different parts of your application. This is where a predictable state container like Redux shines.
Combining AG-Grid with Redux allows you to centralize your grid's state, making it more predictable, easier to debug, and simpler to manage across multiple components. In this post, we'll walk through setting up an AG-Grid instance with Redux, demonstrating how to manage its core data state (rowData) using the Redux pattern.
Why Use Redux with AG-Grid?
While AG-Grid can manage its internal state perfectly well, integrating it with Redux provides several key advantages:
- Centralized State: All your grid's data and relevant configuration can live in a single, predictable store.
- Predictability: State changes are explicit and follow a strict pattern (actions -> reducers -> new state), making it easier to understand how and why your grid updates.
- Debugging: Tools like Redux DevTools offer powerful insights into state changes over time, greatly simplifying debugging complex data flows.
- Scalability: As your application grows and more components need to interact with or display the same grid data, Redux provides a robust mechanism to share that state consistently.
- Separation of Concerns: Your React components focus on rendering, while Redux handles the data logic and state mutations.
Prerequisites
Before diving in, ensure you have a basic understanding of:
- React: Components, props, state, and hooks (
useState,useEffect,useSelector,useDispatch). - Redux Core Concepts: Store, reducers, actions, and the unidirectional data flow.
- AG-Grid Basics: How to render a grid, define columns, and provide row data.
Setting Up Your Project
First, let's create a new React project (if you don't have one) and install the necessary dependencies:
npx create-react-app ag-grid-redux-demo
cd ag-grid-redux-demo
npm install ag-grid-community ag-grid-react redux react-redux
# or yarn add ag-grid-community ag-grid-react redux react-redux
Redux Store Configuration
We'll start by defining our Redux store, including actions, reducers, and the store itself.
1. Actions and Action Types
Actions are plain JavaScript objects that describe what happened. Let's create an action to fetch grid data and another to update it.
Create src/redux/actions/gridActions.js:
// Action Types
export const FETCH_GRID_DATA_REQUEST = 'FETCH_GRID_DATA_REQUEST';
export const FETCH_GRID_DATA_SUCCESS = 'FETCH_GRID_DATA_SUCCESS';
export const FETCH_GRID_DATA_FAILURE = 'FETCH_GRID_DATA_FAILURE';
export const UPDATE_GRID_ROW = 'UPDATE_GRID_ROW';
export const ADD_GRID_ROW = 'ADD_GRID_ROW';
export const DELETE_GRID_ROW = 'DELETE_GRID_ROW';
// Action Creators
export const fetchGridDataRequest = () => ({
type: FETCH_GRID_DATA_REQUEST,
});
export const fetchGridDataSuccess = (data) => ({
type: FETCH_GRID_DATA_SUCCESS,
payload: data,
});
export const fetchGridDataFailure = (error) => ({
type: FETCH_GRID_DATA_FAILURE,
payload: error,
});
export const updateGridRow = (id, updatedRow) => ({
type: UPDATE_GRID_ROW,
payload: { id, updatedRow },
});
export const addGridRow = (newRow) => ({
type: ADD_GRID_ROW,
payload: newRow,
});
export const deleteGridRow = (id) => ({
type: DELETE_GRID_ROW,
payload: id,
});
// An example async action for fetching data (requires redux-thunk for real async,
// but for simplicity here we simulate it or fetch directly in component for now)
export const getGridData = () => async (dispatch) => {
dispatch(fetchGridDataRequest());
try {
// Simulate API call
const response = await new Promise(resolve =>
setTimeout(() => {
const mockData = [
{ id: 'a1', make: 'Toyota', model: 'Celica', price: 35000 },
{ id: 'b2', make: 'Ford', model: 'Mondeo', price: 32000 },
{ id: 'c3', make: 'Porsche', model: 'Boxster', price: 72000 },
];
resolve(mockData);
}, 500)
);
dispatch(fetchGridDataSuccess(response));
} catch (error) {
dispatch(fetchGridDataFailure(error.message));
}
};
2. Reducer
The reducer specifies how the application's state changes in response to actions sent to the store. Our gridReducer will manage the rowData and loading state.
Create src/redux/reducers/gridReducer.js:
import {
FETCH_GRID_DATA_REQUEST,
FETCH_GRID_DATA_SUCCESS,
FETCH_GRID_DATA_FAILURE,
UPDATE_GRID_ROW,
ADD_GRID_ROW,
DELETE_GRID_ROW,
} from '../actions/gridActions';
const initialState = {
rowData: [],
loading: false,
error: null,
};
const gridReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_GRID_DATA_REQUEST:
return {
...state,
loading: true,
error: null,
};
case FETCH_GRID_DATA_SUCCESS:
return {
...state,
loading: false,
rowData: action.payload,
};
case FETCH_GRID_DATA_FAILURE:
return {
...state,
loading: false,
error: action.payload,
};
case UPDATE_GRID_ROW:
return {
...state,
rowData: state.rowData.map(row =>
row.id === action.payload.id
? { ...row, ...action.payload.updatedRow }
: row
),
};
case ADD_GRID_ROW:
return {
...state,
rowData: [...state.rowData, action.payload],
};
case DELETE_GRID_ROW:
return {
...state,
rowData: state.rowData.filter(row => row.id !== action.payload),
};
default:
return state;
}
};
export default gridReducer;
3. Root Reducer and Store
If you have multiple reducers, you'd combine them here. For this example, we'll just use our single gridReducer.
Create src/redux/store.js:
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { thunk } from 'redux-thunk'; // Use named export for Redux Thunk
import gridReducer from './reducers/gridReducer';
const rootReducer = combineReducers({
grid: gridReducer,
});
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
4. Provide the Store to React
Wrap your application with the Provider component from react-redux.
Modify src/index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
Integrating AG-Grid with React-Redux
Now that our Redux store is set up, let's create a React component that displays our AG-Grid and connects to the Redux store.
Create a new component, src/components/MyGrid.js:
import React, { useEffect, useMemo, useCallback, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import {
getGridData,
addGridRow,
updateGridRow,
deleteGridRow
} from '../redux/actions/gridActions';
const MyGrid = () => {
const dispatch = useDispatch();
const { rowData, loading, error } = useSelector((state) => state.grid);
const [gridApi, setGridApi] = useState(null);
// Fetch data when the component mounts
useEffect(() => {
dispatch(getGridData());
}, [dispatch]);
const columnDefs = useMemo(() => [
{ field: 'id', headerName: 'ID', width: 80, editable: false },
{ field: 'make', headerName: 'Make', editable: true },
{ field: 'model', headerName: 'Model', editable: true },
{ field: 'price', headerName: 'Price', editable: true, valueFormatter: p => '$' + p.value.toLocaleString() },
], []);
// Default column properties
const defaultColDef = useMemo(() => ({
flex: 1,
minWidth: 100,
resizable: true,
sortable: true,
filter: true,
}), []);
const onGridReady = useCallback((params) => {
setGridApi(params.api);
}, []);
// Handle cell value changes and dispatch update action
const onCellValueChanged = useCallback((event) => {
const { id, ...updatedFields } = event.data;
dispatch(updateGridRow(id, updatedFields));
}, [dispatch]);
// Example functions to dispatch actions from UI
const handleAddRow = useCallback(() => {
const newRow = {
id: 'd' + Math.floor(Math.random() * 1000), // Unique ID
make: 'New Make',
model: 'New Model',
price: Math.floor(Math.random() * 100000)
};
dispatch(addGridRow(newRow));
if (gridApi) {
gridApi.applyTransaction({ add: [newRow] });
gridApi.ensureIndexVisible(rowData.length); // Scroll to new row
}
}, [dispatch, gridApi, rowData]);
const handleDeleteSelectedRows = useCallback(() => {
if (!gridApi) return;
const selectedNodes = gridApi.getSelectedNodes();
const selectedIds = selectedNodes.map(node => node.data.id);
if (selectedIds.length === 0) {
alert('Please select rows to delete.');
return;
}
selectedIds.forEach(id => dispatch(deleteGridRow(id)));
gridApi.applyTransaction({ remove: selectedNodes.map(node => node.data) });
}, [dispatch, gridApi]);
if (loading) return <div>Loading grid data...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div style={{ width: '100%', height: '500px' }}>
<h2>AG-Grid with Redux State</h2>
<div style={{ marginBottom: '10px' }}>
<button onClick={handleAddRow} style={{ marginRight: '10px' }}>Add New Row</button>
<button onClick={handleDeleteSelectedRows}>Delete Selected Rows</button>
</div>
<div className="ag-theme-alpine" style={{ height: 'calc(100% - 70px)', width: '100%' }}>
<AgGridReact
rowData={rowData} // Row data comes from Redux store
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onGridReady={onGridReady}
onCellValueChanged={onCellValueChanged}
rowSelection="multiple" // Enable row selection for deletion
animateRows={true}
></AgGridReact>
</div>
</div>
);
};
export default MyGrid;
Now, update your src/App.js to render MyGrid:
import './App.css';
import MyGrid from './components/MyGrid';
function App() {
return (
<div className="App" style={{ padding: '20px' }}>
<MyGrid />
</div>
);
}
export default App;
Explanation of the Integration
-
Fetching Data:
In
MyGrid.js, theuseEffecthook dispatches thegetGridDataaction when the component mounts. This asynchronous action simulates an API call and, upon success, dispatchesFETCH_GRID_DATA_SUCCESSwith the fetched data.The
useSelector((state) => state.grid)hook then extractsrowData,loading, anderrorfrom the Redux store'sgridslice. -
Rendering Data:
The
rowDataprop of<AgGridReact />is directly bound to therowDataarray from our Redux store. When the store updates, the component re-renders, and AG-Grid automatically reflects the changes. -
Updating Data:
We've enabled editing on the grid columns (
editable: true). When a cell's value changes, theonCellValueChangedcallback fires. Inside this callback, we dispatch anUPDATE_GRID_ROWaction with the ID and the updated fields. OurgridReducerthen immutably updates the corresponding row in the Redux state. -
Adding/Deleting Data:
Buttons are provided to demonstrate adding and deleting rows. These functions dispatch
ADD_GRID_ROWandDELETE_GRID_ROWactions, respectively. After the Redux state is updated, we also usegridApi.applyTransactionto update AG-Grid's internal state directly. While AG-Grid would eventually re-render from therowDataprop change, usingapplyTransactioncan provide a smoother UI experience for row-level operations. -
useMemoanduseCallback: These hooks are used to memoizecolumnDefs,defaultColDef, and event handlers (onGridReady,onCellValueChanged, etc.). This is important for performance, preventing unnecessary re-renders of AG-Grid, especially since AG-Grid expects stable props.
Advanced Considerations
- Optimizing Performance: For very large datasets or frequent updates, consider using Reselect with
useSelectorto create memoized selectors. This ensures that your components only re-render when the specific slice of state they depend on actually changes, not just any state change in the store. - Managing Grid State: Beyond
rowData, you could also manage column definitions, filters, sort models, and even pagination state within Redux. This allows for persistent grid states or sharing specific grid views across users. - Middleware for API Calls: For more complex asynchronous operations (like authenticating before fetching data, or handling multiple chained API calls), consider using Redux Saga or Redux Thunk (which we used in a simple way for
getGridData) to manage side effects. - Undo/Redo Functionality: With a centralized and predictable state, implementing undo/redo features becomes much more manageable by tracking historical states within your Redux store.
Conclusion
Integrating AG-Grid with Redux provides a robust and scalable solution for managing complex grid state in your React applications. By centralizing data, enforcing a predictable flow, and leveraging powerful debugging tools, you can build more maintainable and feature-rich data grids. This approach empowers you to handle everything from simple data display to intricate CRUD operations with confidence and clarity.
Experiment with extending this setup to manage other aspects of your AG-Grid configuration in Redux, and unlock the full potential of this powerful combination!