This chapter describes how we can implement CRUD functionalities in our frontend. We are going to use the components that we learned about in Chapter 9, Useful Third-Party Components for React. We will fetch data from our backend and present the data in a table. Then, we will implement the delete, edit, and add functionalities. In the final part of this chapter, we will add features so that we can export data to a CSV file.
In this chapter, we will cover the following topics:
The Spring Boot application that we created in Chapter 10, Setting Up the Frontend for Our Spring Boot RESTful Web Service (the unsecured backend), is required, as is the React app that we created in the same chapter (carfront).
The following GitHub link will also be required: https://github.com/PacktPublishing/Full-Stack-Development-with-Spring-Boot-and-React/tree/main/Chapter11.
Check out the following video to see the Code in Action: https://bit.ly/3z78Fcj
In the first phase, we will create the list page to show cars with paging, filtering, and sorting features. Run your unsecured Spring Boot backend. The cars can be fetched by sending the GET request to the http://localhost:8080/api/cars URL, as shown in Chapter 4, Creating a RESTful Web Service with Spring Boot.
Now, let's inspect the JSON data from the response. The array of cars can be found in the _embedded.cars node of the JSON response data.
Once we know how to fetch cars from the backend, we are ready to implement the list page to show the cars. The following steps describe this in practice:
import React from 'react';
function Carlist() {
return(
<div></div>
);
}
export default Carlist;
import React, { useState } from 'react';
function Carlist() {
const [cars, setCars] = useState([]);
return(
<div></div>
);
}
export default Carlist;
import React, { useEffect, useState } from 'react';
function Carlist() {
const [cars, setCars] = useState([]);
useEffect(() => {
fetch('http://localhost:8080/api/cars')
.then(response => response.json())
.then(data => setCars(data._embedded.cars))
.catch(err => console.error(err));
}, []);
return(
<div></div>
);
}
export default Carlist;
return(
<div>
<table>
<tbody>
{
cars.map((car, index) =>
<tr key={index}>
<td>{car.brand}</td>
<td>{car.model}</td>
<td>{car.color}</td>
<td>{car.year}</td>
<td>{car.price}</td>
</tr>)
}
</tbody>
</table>
</div>
);
import './App.css';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Carlist from './components/Carlist';
function App() {
return (
<div className="App">
<AppBar position="static">
<Toolbar>
<Typography variant="h6">
Carshop
</Typography>
</Toolbar>
</AppBar>
<Carlist />
</div>
);
}
export default App;
The server URL address can repeat multiple times when we create more CRUD functionalities, and it will change when the backend is deployed to a server other than the local host; therefore, it is better to define it as a constant. Then, when the URL value changes, we have to modify it in one place. With create-react-app, you can also create a .env file in the root of your project and define environment variables there, but that is not covered here.
Let's create a new file, constants.js, to the src folder of our app:
export const SERVER_URL='http://localhost:8080/';
//Carlist.js
// Import server url (named import)
import { SERVER_URL } from '../constants.js'
// Use imported constant in the fetch method
fetch(SERVER_URL + 'api/cars')
import React, { useEffect, useState } from 'react';
import { SERVER_URL } from '../constants.js';
function Carlist() {
const [cars, setCars] = useState([])
useEffect(() => {
fetch(SERVER_URL + 'api/cars')
.then(response => response.json())
.then(data => setCars(data._embedded.cars))
.catch(err => console.error(err));
}, []);
return(
<div>
<table>
<tbody>
{
cars.map((car, index) =>
<tr key={index}>
<td>{car.brand}</td>
<td>{car.model}</td>
<td>{car.color}</td>
<td>{car.year}</td>
<td>{car.price}</td>
</tr>)
}
</tbody>,
</table>
</div>
);
}
export default Carlist;
We have already used the ag-grid component to implement data grid and that can be used here as well. But, we will use the new MUI data grid component to get the paging, filtering, and sorting features out of the box:
npm install @mui/x-data-grid
import { DataGrid } from '@mui/x-data-grid';
const columns = [
{field: 'brand', headerName: 'Brand', width: 200},
{field: 'model', headerName: 'Model', width: 200},
{field: 'color', headerName: 'Color', width: 200},
{field: 'year', headerName: 'Year', width: 150},
{field: 'price', headerName: 'Price', width: 150},
];
return(
<div style={{ height: 500, width: '100%' }}>
<DataGrid
rows={cars}
columns={columns}
getRowId={row => row._links.self.href}/>
</div>
);
Data grid columns can be filtered using the column menu and clicking the Filter menu item. You can also set the visibility of the columns from the column menu:
Next, we will implement the delete functionality.
Items can be deleted from the database by sending the DELETE method request to the http://localhost:8080/api/cars/{carId} endpoint. If we look at the JSON response data, we can see that each car contains a link to itself and it can be accessed from the _links.self.href node, as shown in the following screenshot. We already used the link field to set a unique ID for every row in the grid. That row ID can be used in deletion, as we can see later:
The following steps demonstrate how to implement the delete functionality:
Let's add a new column to the table using renderCell to render the button element. The row argument that is passed to the function is a row object that contains all values from a row. In our case, it contains a link to a car in each row, and that is needed in deletion. The link is in the row's id property, and we will pass this value to a delete function. Refer to the following source code. We don't want to enable sorting and filtering for the button column, therefore, the filterable and sortable props are set to false. The button invokes the onDelClick function when pressed and passes a link (row.id) to the function as an argument:
const columns = [
{field: 'brand', headerName: 'Brand', width: 200},
{field: 'model', headerName: 'Model', width: 200},
{field: 'color', headerName: 'Color', width: 200},
{field: 'year', headerName: 'Year', width: 150},
{field: 'price', headerName: 'Price', width: 150},
{
field: '_links.self.href',
headerName: '',
sortable: false,
filterable: false,
renderCell: row =>
<button
onClick={() => onDelClick(row.id)}>Delete
</button>
}
];
useEffect(() => {
fetchCars();
}, []);
const fetchCars = () => {
fetch(SERVER_URL + 'api/cars')
.then(response => response.json())
.then(data => setCars(data._embedded.cars))
.catch(err => console.error(err));
}
const onDelClick = (url) => {
fetch(url, {method: 'DELETE'})
.then(response => fetchCars())
.catch(err => console.error(err))
}
When you start your app, the frontend should look like the following screenshot. The car disappears from the list when the Delete button is pressed. Note that after deletions, you can restart the backend to reset the database:
You can also see that when you click any row in the grid, the row is selected. You can disable that by setting the disableSelectionOnClick prop in the grid to true:
<DataGrid
rows={cars}
columns={columns}
disableSelectionOnClick={true}
getRowId={row => row._links.self.href} />
It would be nice to show the user some feedback in the case of successful deletion, or if there are any errors.
import Snackbar from '@mui/material/Snackbar';
The Snackbar component open prop value is a Boolean, and if it is true, the component is shown. Let's declare one state called open to handle the visibility of our Snackbar component. The initial value is false because the message is shown only after the deletion:
//Carlist.js
const [open, setOpen] = useState(false);
<Snackbar
open={open}
autoHideDuration={2000}
onClose={() => setOpen(false)}
message="Car deleted"
/>
const onDelClick = (url) => {
fetch(url, {method: 'DELETE'})
.then(response => {
fetchCars();
setOpen(true);
})
.catch(err => console.error(err))
}
Now, you will see the toast message when the car is deleted, as shown in the following screenshot:
const onDelClick = (url) => {
if (window.confirm("Are you sure to delete?")) {
fetch(url, {method: 'DELETE'})
.then(response => {
fetchCars()
setOpen(true);
})
.catch(err => console.error(err))
}
}
If you press the Delete button now, the confirmation dialog will be opened and the car will only be deleted if you press the OK button:
Finally, we will also check the response status that everything went fine in the deletion. As we have already learned, the response object has the ok property, which we can use to check that the response was successful:
const onDelClick = (url) => {
if (window.confirm("Are you sure to delete?")) {
fetch(url, {method: 'DELETE'})
.then(response => {
if (response.ok) {
fetchCars();
setOpen(true);
}
else {
alert('Something went wrsong!');
}
})
.catch(err => console.error(err))
}
}
Next, we will begin the implementation of the functionality to add a new car.
The next step is to create an add functionality for the frontend. We will implement this using the MUI modal dialog. We already went through the utilization of the MUI modal form in Chapter 9, Useful Third-Party Components for React. We will add the New Car button to the user interface, which opens the modal form when it is pressed. The modal form contains all the fields that are required to add a new car, as well as the button for saving and canceling.
We have already installed the MUI component library to our frontend app in Chapter 10, Setting Up the Frontend for Our Spring Boot RESTful Web Service.
The following steps show you how to create the add functionality using the modal dialog component:
import React from 'react';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
function AddCar(props) {
return(
<div></div>
);
}
export default AddCar;
import React, { useState } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
function AddCar(props) {
const [open, setOpen] = useState(false);
const [car, setCar] = useState({
brand: '',
model: '',
color: '',
year: '',
fuel: '',
price: ''
});
return(
<div></div>
);
}
export default AddCar;
// AddCar.js
// Open the modal form
const handleClickOpen = () => {
setOpen(true);
};
// Close the modal form
const handleClose = () => {
setOpen(false);
};
// AddCar.js
const handleChange = (event) => {
setCar({...car, [event.target.name]:
event.target.value});
}
return(
<div>
<button onClick={handleClickOpen}>New Car</button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>New car</DialogTitle>
<DialogContent>
<input placeholder="Brand" name="brand"
value={car.brand} onChange={handleChange}
/><br/>
<input placeholder="Model" name="model"
value={car.model} onChange={handleChange}
/><br/>
<input placeholder="Color" name="color"
value={car.color} onChange={handleChange}/>
<br/>
<input placeholder="Year" name="year"
value={car.year} onChange={handleChange}/>
<br/>
<input placeholder="Price" name="price"
value={car.price} onChange={handleChange}/>
<br/>
</DialogContent>
<DialogActions>
<button onClick={handleClose}>Cancel</button>
<button onClick={handleClose}>Save</button>
</DialogActions>
</Dialog>
</div>
);
// Carlist.js
// Add a new car
const addCar = (car) => {
fetch(SERVER_URL + 'api/cars',
{
method: 'POST',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify(car)
})
.then(response => {
if (response.ok) {
fetchCars();
}
else {
alert('Something went wrong!');
}
})
.catch(err => console.error(err))
}
import AddCar from './AddCar.js';
// Carlist.js
return(
<React.Fragment>
<AddCar addCar={addCar} />
<div style={{ height: 500, width: '100%' }}>
<DataGrid
rows={cars}
columns={columns}
disableSelectionOnClick={true}
getRowId={row => row._links.self.href}
/>
<Snackbar
open={open}
autoHideDuration={2000}
onClose={() => setOpen(false)}
message="Car deleted"
/>
</div>
</React.Fragment>
);
// AddCar.js
// Save car and close modal form
const handleSave = () => {
props.addCar(car);
handleClose();
}
// AddCar.js
<DialogActions>
<button onClick={handleClose}>Cancel</button>
<button onClick={handleSave}>Save</button>
</DialogActions>
After saving, the list page is refreshed, and the new car can be seen in the list:
Next, we will begin to implement the edit functionality in relation to our frontend.
We will implement the edit functionality by adding the Edit button to each table row. When the row Edit button is pressed, it opens the modal form, where the user can edit the existing car and finally save the changes:
import React, { useState } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
function EditCar(props) {
const [open, setOpen] = useState(false);
const [car, setCar] = useState({
brand: '', model: '', color: '',
year: '', fuel:'', price: ''
});
// Open the modal form
const handleClickOpen = () => {
setOpen(true);
};
// Close the modal form
const handleClose = () => {
setOpen(false);
};
const handleChange = (event) => {
setCar({...car,
[event.target.name]: event.target.value});
}
// Update car and close modal form
const handleSave = () => {
}
return(
<div></div>
);
}
export default EditCar;
// EditCar.js
return(
<div>
<button onClick={handleClickOpen}>Edit</button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Edit car</DialogTitle>
<DialogContent>
<input placeholder="Brand" name="brand"
value={car.brand}onChange={handleChange}
/><br/>
<input placeholder="Model" name="model"
value={car.model}onChange={handleChange}
/><br/>
<input placeholder="Color" name="color"
value={car.color}onChange={handleChange}
/><br/>
<input placeholder="Year" name="year"
value={car.year} onChange={handleChange}/><br/>
<input placeholder="Price" name="price"
value={car.price}onChange={handleChange}
/><br/>
</DialogContent>
<DialogActions>
<button onClick={handleClose}> Cancel
</button>
<button onClick={handleSave}>Save</button>
</DialogActions>
</Dialog>
</div>
);
The function gets two arguments—the updated car object and the request URL. Following a successful update, we will fetch the cars and the list is updated:
// Carlist.js
// Update car
const updateCar = (car, link) => {
fetch(link,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(car)
})
.then(response => {
if (response.ok) {
fetchCars();
}
else {
alert('Something went wrong!');
}
})
.catch(err => console.error(err))
}
import EditCar from './EditCar.js';
// Carlist.js
const columns = [
{field: 'brand', headerName: 'Brand', width: 200},
{field: 'model', headerName: 'Model', width: 200},
{field: 'color', headerName: 'Color', width: 200},
{field: 'year', headerName: 'Year', width: 150},
{field: 'price', headerName: 'Price', width: 150},
{
field: '_links.car.href',
headerName: '',
sortable: false,
filterable: false,
renderCell: row =>
<EditCar
data={row}
updateCar={updateCar} />
},
{
field: '_links.self.href',
headerName: '',
sortable: false,
filterable: false,
renderCell: row =>
<button
onClick={() =>
onDelClick(row.id)}>Delete
</button>
}
];
// EditCar.js
// Open the modal form and update the car state
const handleClickOpen = () => {
setCar({
brand: props.data.row.brand,
model: props.data.row.model,
color: props.data.row.color,
year: props.data.row.year,
fuel: props.data.row.fuel,
price: props.data.row.price
})
setOpen(true);
}
// EditCar.js
// Update car and close modal form
const handleSave = () => {
props.updateCar(car, props.data.id);
handleClose();
}
Now, we have implemented all CRUD functionalities in relation to our frontend.
One feature that we will also implement is a comma-separated values (CSV) export of the data. We don't need any extra library for the export because the MUI data grid provides this feature:
import { DataGrid, GridToolbarContainer, GridToolbarExport,
gridClasses } from '@mui/x-data-grid';
// Carlist.js
function CustomToolbar() {
return (
<GridToolbarContainer
className={gridClasses.toolbarContainer}>
<GridToolbarExport />
</GridToolbarContainer>
);
}
return(
<React.Fragment>
<AddCar addCar={addCar} />
<div style={{ height: 500, width: '100%' }}>
<DataGrid
rows={cars}
columns={columns}
disableSelectionOnClick={true}
getRowId={row => row._links.self.href}
components={{ Toolbar: CustomToolbar }}
/>
<Snackbar
open={open}
autoHideDuration={2000}
onClose={() => setOpen(false)}
message="Car deleted"
/>
</div>
</React.Fragment>
);
Now, you will see the Export button in the grid. If you press the button and select Download as CSV, the grid data is exported to a CSV file. You can also print your grid using the Export button:
Now, all the functionalities have been implemented. In Chapter 12, Styling the Frontend with React MUI, we will focus on styling the frontend.
In this chapter, we implemented all the functionalities for our app. We started with fetching the cars from the backend and showing these in the MUI data grid, which provides paging, sorting, and filtering features. Then, we implemented the delete functionality and used the toast component to give feedback to the user.
The add and edit functionalities were implemented using the MUI modal dialog component. Finally, we implemented the ability to export data to a CSV file.
In the next chapter, we are going to style the rest of our frontend using the React MUI component library.
Packt has other great resources available for learning about React. These are as follows:
3.143.4.181