AG-Grid-React-Series-#61: Building a CRUD App with AG-Grid
Unlock the full potential of AG-Grid in your React applications by mastering CRUD (Create, Read, Update, Delete) operations. This guide will walk you through building a dynamic data grid that allows users to interact with data seamlessly, from displaying records to adding new entries, editing existing ones, and deleting rows.
Prerequisites
Before diving in, ensure you have a basic understanding of:
- React Fundamentals: Components, state, props, and hooks (
useState,useEffect,useRef,useCallback). - AG-Grid Basics: How to set up a grid, define column definitions, and pass data.
- Node.js and npm/yarn: For project setup and package management.
Setting Up Your AG-Grid React Project
If you don't already have a React project with AG-Grid set up, follow these quick steps:
npx create-react-app ag-grid-crud-app
cd ag-grid-crud-app
npm install ag-grid-community ag-grid-react
# or yarn add ag-grid-community ag-grid-react
Next, you'll need to import the necessary styles in your src/App.js or a dedicated stylesheet:
import 'ag-grid-community/styles/ag-grid.css'; // Core grid CSS
import 'ag-grid-community/styles/ag-theme-alpine.css'; // Theme (e.g., Alpine)
The Core Data Model
For our CRUD application, we'll use a simple dataset of car information. Each row should ideally have a unique identifier. This is crucial for efficiently performing update and delete operations.
const initialCarData = [
{ id: 1, make: 'Toyota', model: 'Celica', price: 35000 },
{ id: 2, make: 'Ford', model: 'Mondeo', price: 32000 },
{ id: 3, make: 'Porsche', model: 'Boxster', price: 72000 },
];
1. The "Read" Operation: Displaying Data
The first step in any CRUD application is to display the data. We'll set up our AgGridReact component, define columns, and populate it with initial data.
We'll use useState to manage our rowData and columnDefs, and useRef to gain access to the grid's API after it's ready.
import React, { useState, useEffect, 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';
function App() {
const gridRef = useRef(); // For accessing grid API
const [rowData, setRowData] = useState([]);
// Column Definitions: Defines the columns to be displayed.
// We'll make 'make', 'model', and 'price' editable later for the Update operation.
const [columnDefs] = useState([
{ field: 'id', headerName: 'ID', width: 80 },
{ field: 'make', headerName: 'Make', editable: true },
{ field: 'model', headerName: 'Model', editable: true },
{ field: 'price', headerName: 'Price', editable: true, type: 'numericColumn' },
]);
// This function is called once the grid is ready.
// We simulate fetching initial data here. In a real app, this would be an API call.
const onGridReady = useCallback((params) => {
// params.api holds the grid API, which we store in the ref.
// gridRef.current.api = params.api; // Not strictly needed if using params directly, but good for other uses.
setTimeout(() => {
setRowData([
{ id: 1, make: 'Toyota', model: 'Celica', price: 35000 },
{ id: 2, make: 'Ford', model: 'Mondeo', price: 32000 },
{ id: 3, make: 'Porsche', model: 'Boxster', price: 72000 },
]);
}, 500); // Simulate network delay
}, []);
return (
<div style={{ width: '100%', height: 'calc(100vh - 20px)' }}>
<div className="ag-theme-alpine" style={{ height: '400px', width: '900px' }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
onGridReady={onGridReady}
rowSelection="multiple" // Enable row selection for delete
animateRows={true} // For smoother row animations
></AgGridReact>
</div>
</div>
);
}
export default App;
At this point, you should see an AG-Grid instance displaying the initial car data.
2. The "Create" Operation: Adding New Rows
To enable users to add new data, we'll implement a button that, when clicked, inserts a new row into the grid. AG-Grid provides the api.applyTransaction() method for this, which is highly efficient for modifying row data.
// ... (inside App component)
const onAddRow = useCallback(() => {
// Generate a simple unique ID for the new row.
// In a real application, this ID would typically come from the backend after creation.
const newId = rowData.length > 0 ? Math.max(...rowData.map(r => r.id)) + 1 : 1;
const newRow = { id: newId, make: 'New Make', model: 'New Model', price: 0 };
// Use applyTransaction to add the new row to the grid
gridRef.current.api.applyTransaction({
add: [newRow],
addIndex: 0 // Optional: add at the top
});
// Update the local React state to keep it in sync
setRowData(prevData => [newRow, ...prevData]);
console.log('Added new row:', newRow);
// In a real app, you would make an API call here to save the newRow to your backend:
// apiService.createCar(newRow).then(response => {
// // Update the row with the actual ID from the backend if different
// });
}, [rowData]); // dependency on rowData ensures newId calculation is based on latest state
return (
<div style={{ width: '100%', height: 'calc(100vh - 20px)' }}>
<button onClick={onAddRow} style={{ marginBottom: '10px', padding: '8px 15px' }}>
Add New Car
</button>
<div className="ag-theme-alpine" style={{ height: '400px', width: '900px' }}>
{/* AgGridReact component here */}
</div>
</div>
);
}
// ... (export App)
The applyTransaction method is powerful because it handles animations and efficiently updates the grid's internal state.
3. The "Update" Operation: Editing Existing Data
AG-Grid makes editing data straightforward. By setting editable: true in your columnDefs, users can double-click cells to edit them. To synchronize these changes with your application's state and potentially a backend, you'll listen to the onCellValueChanged event.
// ... (inside App component)
// Ensure your columnDefs have editable: true for the desired columns:
// const [columnDefs] = useState([
// { field: 'id', headerName: 'ID', width: 80 },
// { field: 'make', headerName: 'Make', editable: true },
// { field: 'model', headerName: 'Model', editable: true },
// { field: 'price', headerName: 'Price', editable: true, type: 'numericColumn' },
// ]);
const onCellValueChanged = useCallback((event) => {
console.log('Cell value changed:', event.data);
// event.data contains the updated row object.
// AG-Grid automatically updates its internal state if editable is true.
// Here, you would typically make an API call to your backend to save the changes:
// apiService.updateCar(event.data.id, event.data).then(response => {
// console.log('Backend updated successfully!', response);
// }).catch(error => {
// console.error('Failed to update car:', error);
// // Optional: Revert changes in grid if backend update fails
// // gridRef.current.api.refreshCells({ force: true });
// });
// Update the local React state to keep it in sync
setRowData(prevData =>
prevData.map(row => (row.id === event.data.id ? event.data : row))
);
}, []);
return (
<div style={{ width: '100%', height: 'calc(100vh - 20px)' }}>
{/* ... buttons ... */}
<div className="ag-theme-alpine" style={{ height: '400px', width: '900px' }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
onGridReady={onGridReady}
onCellValueChanged={onCellValueChanged} // Listen for cell changes
rowSelection="multiple"
animateRows={true}
></AgGridReact>
</div>
</div>
);
}
// ... (export App)
The event.data object passed to onCellValueChanged will contain the entire row with the updated value. This simplifies sending the data to your backend.
4. The "Delete" Operation: Removing Rows
Deleting rows typically involves selecting one or more rows and then confirming their removal. We'll add another button that, when clicked, deletes all currently selected rows from the grid using api.applyTransaction({ remove: [...] }).
// ... (inside App component)
const onDeleteSelected = useCallback(() => {
const selectedRows = gridRef.current.api.getSelectedRows();
if (selectedRows.length === 0) {
alert('Please select rows to delete.');
return;
}
if (window.confirm(`Are you sure you want to delete ${selectedRows.length} selected row(s)?`)) {
// Use applyTransaction to remove selected rows from the grid
gridRef.current.api.applyTransaction({ remove: selectedRows });
// Update the local React state to keep it in sync
setRowData(prevData =>
prevData.filter(row => !selectedRows.some(sRow => sRow.id === row.id))
);
console.log('Deleted rows:', selectedRows);
// In a real app, you would make an API call to delete from your backend:
// const idsToDelete = selectedRows.map(row => row.id);
// apiService.deleteCars(idsToDelete).then(response => {
// console.log('Backend delete successful!', response);
// }).catch(error => {
// console.error('Failed to delete cars:', error);
// // Optional: Revert changes if backend delete fails
// // gridRef.current.api.applyTransaction({ add: selectedRows });
// });
}
}, []);
return (
<div style={{ width: '100%', height: 'calc(100vh - 20px)' }}>
<button onClick={onAddRow} style={{ marginBottom: '10px', marginRight: '10px', padding: '8px 15px' }}>
Add New Car
</button>
<button onClick={onDeleteSelected} style={{ marginBottom: '10px', padding: '8px 15px', backgroundColor: '#dc3545', color: 'white', border: 'none' }}>
Delete Selected Rows
</button>
<div className="ag-theme-alpine" style={{ height: '400px', width: '900px', marginTop: '10px' }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
onGridReady={onGridReady}
onCellValueChanged={onCellValueChanged}
rowSelection="multiple" // Essential for selecting multiple rows
animateRows={true}
></AgGridReact>
</div>
</div>
);
}
// ... (export App)
Remember that rowSelection="multiple" on the AgGridReact component is necessary for users to be able to select multiple rows for deletion.
Putting It All Together: The Complete App.js
Here's the complete React component integrating all the CRUD operations discussed:
import React, { useState, useEffect, 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';
function App() {
const gridRef = useRef();
const [rowData, setRowData] = useState([]);
const [columnDefs] = useState([
{ field: 'id', headerName: 'ID', width: 80 },
{ field: 'make', headerName: 'Make', editable: true, flex: 1 },
{ field: 'model', headerName: 'Model', editable: true, flex: 1 },
{ field: 'price', headerName: 'Price', editable: true, type: 'numericColumn', flex: 1 },
]);
const defaultColDef = useRef({
sortable: true,
filter: true,
resizable: true,
});
// READ: Simulate fetching initial data on grid ready
const onGridReady = useCallback((params) => {
setTimeout(() => {
setRowData([
{ id: 1, make: 'Toyota', model: 'Celica', price: 35000 },
{ id: 2, make: 'Ford', model: 'Mondeo', price: 32000 },
{ id: 3, make: 'Porsche', model: 'Boxster', price: 72000 },
]);
}, 500);
}, []);
// CREATE: Add a new row
const onAddRow = useCallback(() => {
const newId = rowData.length > 0 ? Math.max(...rowData.map(r => r.id)) + 1 : 1;
const newRow = { id: newId, make: 'New Make', model: 'New Model', price: 0 };
gridRef.current.api.applyTransaction({
add: [newRow],
addIndex: 0
});
setRowData(prevData => [newRow, ...prevData]);
console.log('Added new row:', newRow);
// In a real app, call your API to save the new row
}, [rowData]);
// UPDATE: Handle cell value changes
const onCellValueChanged = useCallback((event) => {
console.log('Cell value changed:', event.data);
setRowData(prevData =>
prevData.map(row => (row.id === event.data.id ? event.data : row))
);
// In a real app, call your API to update the row
}, []);
// DELETE: Delete selected rows
const onDeleteSelected = useCallback(() => {
const selectedRows = gridRef.current.api.getSelectedRows();
if (selectedRows.length === 0) {
alert('Please select rows to delete.');
return;
}
if (window.confirm(`Are you sure you want to delete ${selectedRows.length} selected row(s)?`)) {
gridRef.current.api.applyTransaction({ remove: selectedRows });
setRowData(prevData =>
prevData.filter(row => !selectedRows.some(sRow => sRow.id === row.id))
);
console.log('Deleted rows:', selectedRows);
// In a real app, call your API to delete the rows
}
}, []);
return (
<div style={{ padding: '20px', display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<h1>AG-Grid CRUD Example</h1>
<div style={{ marginBottom: '10px' }}>
<button onClick={onAddRow} style={{ marginRight: '10px', padding: '8px 15px', cursor: 'pointer' }}>
Add New Car
</button>
<button onClick={onDeleteSelected} style={{ padding: '8px 15px', backgroundColor: '#dc3545', color: 'white', border: 'none', cursor: 'pointer' }}>
Delete Selected Rows
</button>
</div>
<div className="ag-theme-alpine" style={{ height: '400px', width: '900px' }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef.current}
onGridReady={onGridReady}
onCellValueChanged={onCellValueChanged}
rowSelection="multiple"
animateRows={true}
suppressRowClickSelection={false} // Allow row selection via click
></AgGridReact>
</div>
</div>
);
}
export default App;
Best Practices and Considerations
- Backend Integration: In a real-world application, every CRUD operation (Create, Update, Delete) on the frontend would trigger an asynchronous API call to your backend to persist the changes. The examples above include comments indicating where these API calls would typically occur.
- Error Handling: Implement robust error handling for your API calls. If a backend operation fails (e.g., due to validation errors, network issues), you might need to revert the changes in the grid or display an error message to the user.
- Optimistic vs. Pessimistic Updates:
- Optimistic: Update the UI immediately (as shown in this guide) and then send the request to the backend. If the backend call fails, revert the UI. This provides a snappier user experience.
- Pessimistic: Send the request to the backend first. Only update the UI if the backend call succeeds. This is safer but can feel slower.
- Unique Row IDs: Ensure every row has a stable, unique ID. AG-Grid relies heavily on these IDs for efficient rendering and state management during updates and deletions.
- User Experience: Provide clear feedback to the user. For instance, show loading spinners during API calls, success messages after operations, and confirmation dialogs before destructive actions (like deletion).
- State Management: For larger, more complex applications, consider using a dedicated state management library like Redux, Zustand, or React Context to manage your `rowData` consistently across your application.