----This guide
helps you create a Java full stack application with all the CRUD (Create, Read,
Update and Delete) features using React as Frontend framework and Spring Boot
as the backend REST API. We will be using JavaScript as the frontend language
and Java as the backend language.
Important points
to note:
#REST API is exposed using Spring Boot
REST API
is consumed from React Frontend to present the UI #The Database,
in this example, is a hardcoded in-memory static list.
#Getting an
overview of Spring Boot REST API Resources
In this guide,
we will create these services using proper URIs and HTTP methods:
@GetMapping("/instructors/{username}/courses")
: Get Request Method exposing the list of courses taught by a specific
instructor
@GetMapping("/instructors/{username}/courses/{id}")
: Get Request Method exposing the details of a specific course taught by a
specific instructor
@DeleteMapping("/instructors/{username}/courses/{id}")
: Delete Request Method to delete a course belonging to a specific instructor
@PutMapping("/instructors/{username}/courses/{id}")
: Put Request Method to update the course details of a specific course taught
by a specific instructor
@PostMapping("/instructors/{username}/courses")
: Post Request Method to create a new course for a specific instructor
#A few
details:
InstructorApp.jsx
: React Component representing the high-level structure of the application.
Routing is defined in this file.
ListCoursesComponent.jsx
- React Component for listing all the courses for an instructor.
CourseComponent.jsx - React Component for editing Course Details and creating a
new course CourseDataService.js - Service using axios framework to make the
Backend REST API Calls.
#Understanding the
tools you need to build this project Maven 3.0+ for building Spring Boot API
Project
npm, webpack for building frontend
Your favorite IDE. We use Eclipse for Java and Visual Studio Code
for Frontend - JavaScript, TypeScript, Angular and React.
JDK 1.8+
Node v8+
Embedded Tomcat, built into Spring Boot
Starter Web
#Installing Node
Js (npm) & Visual Studio Code
({:target=”_blank”}
Step
01 - Installing NodeJs and NPM - Node Package Manager Step 02 - Quick
Introduction to NPM
Step 03 - Installing Visual Studio Code - Front End
JavaScript Editor
#Full Stack
CRUD application with React and Spring Boot - Step By Step Approach We will use
a step by step approach to creating the full stack application
Create
a Spring Boot Application with Spring Boot Initializr Create a React
application using Create React App
Create
the Retrieve Courses REST API and Enhance the React Front end to retrieve the
courses using the axios framework
Add feature to
delete a course in React front end and Spring Boot REST API
Add functionality to update course details in React front end and
Spring Boot REST API Add feature to
create a course in React front end and Spring Boot REST API
#Step 1:
Bootstrapping Spring Boot REST API with Spring Initializr
Creating a REST service with Spring Initializr is a cake walk. We
will use Spring Web MVC as our web framework.
Spring
Initializr http://start.spring.io/
is great tool to bootstrap your Spring Boot projects.
#Step 2 -
Bootstrapping React Frontend with Create React App
Create
React App is an amazing tool to bootstrap your React applications. Creating
React Frontend Applications with Create React App is very simple.
Launch
up your terminal/command prompt. Make sure that you have node installed. npx
create-react-app frontend-spring-boot-react-crud
#Step
3 - Creating REST API for Retrieve All Courses and Connecting React Frontend #Let’s
start with building the course listing screen.
To be able to do
that, we need to
Create
REST API for retrieving a list of courses. Connect the React Frontend to the
backend REST API
#Create REST
API for retrieving a list of courses
Web
Services, REST and Designing REST API, are pretty deep concepts. We would
recommend to check this out for more - Designing Great REST API
We will create
A model object
Course.java
A Hardcoded Business Service CoursesHardcodedService.java A Resource
to expose the REST API CourseResource.java
We
will start with creating a model object Course.java. The snippet below shows
the content of the model class. For the complete listing, refer
course/Course.java in the complete code example at the end of this article.
public
class Course { private Long id;
private
String username; private String description;
//no arg constructor
//constructor
with 3 args
//getters and
setters
//hashcode and equals
Next, let’s create a Business Service. In
this article, we will use hardcoded data.
@Service
public class CoursesHardcodedService {
private
static List<Course> courses = new ArrayList<>(); private static
long idCounter = 0;
static {
courses.add(new Course(++idCounter, "in28minutes",
"Learn Full stack with Spring Boot and Angular")); courses.add(new
Course(++idCounter, "in28minutes", "Learn Full stack with Spring
Boot and React")); courses.add(new Course(++idCounter,
"in28minutes", "Master Microservices with Spring Boot and Spring
Cloud"));
courses.add(new Course(++idCounter,
"in28minutes",
"Deploy
Spring Boot Microservices to Cloud with Docker and Kubernetes"));
}
public
List<Course> findAll() { return courses;
}
}
Few things to note
Data is
hardcoded
findAll returns the complete list of
courses
You can see that the API of the Service is modelled around the
Spring Data Repository interfaces. If you are familiar with JPA and Spring
Data, you can easily replace this with a Service talking to a database.
Next, let create the REST Resource to retrieve the
list of courses for an instructor.
@RestController
public class CourseResource {
@Autowired
private CoursesHardcodedService
courseManagementService;
@GetMapping("/instructors/{username}/courses")
public
List<Course> getAllCourses(@PathVariable String username) { return
courseManagementService.findAll();
}
}
Few things to note:
@RestController
: Combination of @Controller and @ResponseBody - Beans returned are converted
to/from JSON/XML.
@Autowired
private CoursesHardcodedService courseManagementService - Autowire the
CoursesHardcodedService so that we can retrieve details from business service.
If
you launch up the Spring boot application and go to
http://localhost:8080/instructors/in28minutes/courses in the browser, you would
see the response from the API.
[
{
"id": 1,
"username":
"in28minutes",
"description": "Learn Full
stack with Spring Boot and Angular"
},
{
"id": 2,
"username":
"in28minutes",
"description": "Learn Full
stack with Spring Boot and React"
},
{
"id": 3,
"username":
"in28minutes",
"description":
"Master Microservices with Spring Boot and Spring Cloud"
},
{
"id":
4,
"username":
"in28minutes",
"description": "Deploy
Spring Boot Microservices to Cloud with Docker and Kubernetes"
}
]
We have the REST
API up and running. Its time to focus on the Frontend.
#Enhancing React
App to consume the REST API
To be able to enhance the React
Application to consume the REST API, we would need to
Create
an Application Component - to represent the structure of the complete
application and include it in App.jsx - InstructorApp.jsx
Add
the frameworks need to call the REST API - axios, display a form - formik and
support routing - react-router- dom
Create
a view component for showing a list of course details and include it in the
Application Component - ListCoursesComponent.jsx
Invoking
Retrieve Courses REST API from React Component - To enable this we will create
a service to call the REST API using the axios framework -
CourseDataService.js. ListCoursesComponent.jsx will make use of
CourseDataService.js
Let’s start with creating an Application
Component - InstructorApp.jsx
/src/component/InstructorApp.jsx
import React, {
Component } from 'react';
class
InstructorApp extends Component { render() {
return (
<h1>Instructor
Application</h1>
)
}
}
export
default InstructorApp Few things to note:
One of the first things you would need to understand about React is
the concept of the component. You can find more about a react component here -
React Components
class
InstructorApp extends Component - Every React Class Component should extend a
class called Component.
render()
- The render() method of a component returns what needs to be displayed as part
of the component export default InstructorApp - Each JavaScript file is a
module. If you wanted elements from a JavaScript module to be used in other
JavaScript modules, we would need to export them. Here, we are making
InstructorApp available for import in other components.
Let’s
update the App.js to display the InstructorApp component. src/App.js
import
React, { Component } from 'react'; import './App.css';
import InstructorApp from
'./component/InstructorApp';
class
App extends Component { render() {
return (
<div
className="container">
<InstructorApp
/>
</div>
);
}
}
export default
App;
Few things to note:
import
InstructorApp from './component/InstructorApp' - Importing the InstructorApp
component class
<InstructorApp /> - Display the Instructor App component.
Let’s update the App.css to use Bootstrap framework:
/src/App.css
@import url(https://unpkg.com/bootstrap@4.1.0/dist/css/ #Adding Frameworks
to React Application
In this project, we will make use of axios to execute REST APIs,
react-router-dom to do the Routing between pages and formik to create forms.
Let’s stop the front end app running in the command prompt and execute these
commands.
npm add axios
npm add react-router-dom npm add formik
When commands execute successfully, you would see new
entries in package.json
"axios":
"^0.18.0",
"formik": "^1.5.1",
"react-router-dom":
"^5.0.0",
You can run ‘npm start’ to relaunch the
front end app loading up all the new frameworks.
Creating a List
Courses Component
Let’s create a
new component for showing the List of courses - ListCoursesComponent.jsx. For
now, let’s hardcode a course into the course list.
/src/component/ListCoursesComponent.jsx
class ListCoursesComponent extends Component { render() {
return (
<div
className="container">
<h3>All Courses</h3>
<div
className="container">
<table className="table">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Learn
Full stack with Spring Boot and Angular</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}
}
export
default ListCoursesComponent Things to Note:
It’s
a simple component. Returning a hardcoded table displaying a list of courses.
Let’s update the InstructorApp component to display the ListCoursesComponent.
/src/component/InstructorApp.jsx
class
InstructorApp extends Component { render()
{
return (<>
<h1>Instructor
Application</h1>
<ListCoursesComponent/>
</>
)
}
}
export default
InstructorApp
We are importing the ListCoursesComponent
and displaying it in the InstructorApp.
#Invoking Retrieve
Courses REST API from React Component
We
had created the REST API for retrieving the list of courses earlier. To call
the REST API we would need to use a framework called axios.
#Axios is
a frontend framework that helps you make
REST
API calls with different request methods including GET, POST, PUT, DELETE etc
Intercept Front end REST API calls and add headers and request content
Let’s create a data service method to call the REST
API.
/src/service/CourseDataService.js
import axios from 'axios'
const INSTRUCTOR
= 'in28minutes'
const
COURSE_API_URL = 'http://localhost:8080'
const INSTRUCTOR_API_URL = `${COURSE_API_URL}/instructors/${INSTRUCTOR}`
class CourseDataService {
retrieveAllCourses(name)
{
return
axios.get(`${INSTRUCTOR_API_URL}/courses`);
}
}
export
default new CourseDataService() Important points to note:
const
INSTRUCTOR_API_URL = `${COURSE_API_URL}/instructors/${INSTRUCTOR}` - We are
forming the URL to call in a reusable way.
axios.get(`${INSTRUCTOR_API_URL}/courses`) - Call the
REST API with the GET method.
export default new CourseDataService() - We are creating an instance
of CourseDataService and making it available for other components.
To
make the REST API call, we would need to call the CourseDataService -
retrieveAllCourses method from the ListCoursesComponent
Important
snippets are shown below:
class ListCoursesComponent extends Component { constructor(props) {
super(props)
this.refreshCourses =
this.refreshCourses.bind(this)
}
componentDidMount()
{ this.refreshCourses();
}
refreshCourses()
{ CourseDataService.retrieveAllCourses(INSTRUCTOR)//HARDCODED
.then(
response => {
console.log(response);
}
)
}
....
}
Things to note:
componentDidMount()
- React defines a component lifecycle. componentDidMount will be called as soon
as the component is mounted. We are calling refreshCourses as soon as a
component is mounted. this.refreshCourses = this.refreshCourses.bind(this) -
Any method in a react component should be bound to this.
-CourseDataService.retrieveAllCourses(INSTRUCTOR).then
- This would make the call to the REST API. You can define how to process the
response in the then method.
When you run the react app in the browser right now,
you would see the following errors in the console
[Error] Origin
http://localhost:3000 is not allowed by Access-Control-Allow-Origin.
[Error]
XMLHttpRequest cannot load
http://localhost:8080/instructors/in28minutes/courses due to access control
checks.
[Error]
Failed to load resource: Origin http://localhost:3000 is not allowed by
Access-Control-Allow-Origin. (courses, line 0)
[Error]
Unhandled Promise Rejection: Error: Network Error (anonymous function)
(0.chunk.js:1097) promiseReactionJob
The
Backend Spring Boot REST API is running on http://localhost:8080, and it is not
allowing requests from other servers - http://localhost:3000, in this example.
Let’s configure
Rest Resource to allow access from specific servers.
@CrossOrigin(origins
= { "http://localhost:3000", "http://localhost:4200" })
@RestController
public class CourseResource {
An important
thing to note
@CrossOrigin(origins
= { “http://localhost:3000”, “http://localhost:4200” }) - Allow requests from
specific origins We will use 3000 to run React and Vue JS apps, and we use 4200 to run Angular apps. Hence we are
allowing requests from both ports.
If
you refresh the page again, you would see the response from server printed in
the console. We would need to use the data from the response and show it on the
component.
Following
snippet highlights the significant changes class ListCoursesComponent extends
Component {
constructor(props)
{ super(props) this.state = { courses: [], message: null
}
this.refreshCourses
= this.refreshCourses.bind(this)
}
componentDidMount()
{ this.refreshCourses();
}
refreshCourses()
{ CourseDataService.retrieveAllCourses(INSTRUCTOR)//HARDCODED
.then(
response => {
console.log(response);
this.setState({
courses: response.data })
}
)
}
....
}
Important things
to note:
this.state = {courses: [],message: null} - To display courses, we
need to make them available to the component. We add courses to the state of
the component and initialize it in the constructor.
response
=> {this.setState({ courses: response.data })} - When the response comes
back with data, we update the state.
We
have data in the state. How do we display it? We need to update the render
method.
render()
{ return (
<div className="container">
<h3>All
Courses</h3>
<div
className="container">
<table
className="table">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{
this.state.courses.map( course =>
<tr key={course.id}>
<td>{course.id}</td>
<td>{course.description}</td>
</tr>
)
}
</tbody>
</table>
</div>
</div>
)
}
Important things
to note
this.state.courses.map(
- Allow you to loop around a list of items and define how each item should be
displayed. key={course.id} - A key is used to uniquely identify a row.
{course.id} - In JSX, we use {} to execute JavaScript
code.
#Step 4:
Adding Delete Feature to List Courses Page To be able to do this
We
need a REST API in Spring Boot Backend for deleting a course We would need to
update React frontend to use the API
Adding
Delete Method in the Backend REST API It should be easy.
Snippets
below show how we create a simple deleteById method in CoursesHardcodedService
and expose it from CourseResource.
@Service
public class CoursesHardcodedService {
public
Course deleteById(long id) { Course course = findById(id);
if
(course == null) return null;
if
(courses.remove(course)) { return course;
}
return null;
}
public class CourseResource {
@DeleteMapping("/instructors/{username}/courses/{id}")
public ResponseEntity<Void> deleteCourse(@PathVariable String
username, @PathVariable long id) { Course course =
courseManagementService.deleteById(id);
if (course != null)
{
return
ResponseEntity.noContent().build();
}
return
ResponseEntity.notFound().build();
}
Important things to note:
@DeleteMapping("/instructors/{username}/courses/{id}")
- We are mapping the Delete Request Method with two path variables
@PathVariable
String username, @PathVariable long id - Defining the variables for Path
Variables ResponseEntity.noContent().build() - If Request is successful, return
no content back ResponseEntity.notFound().build() - If delete failed, return
error - resource not found.
Enhancing React app with Delete Course Feature
Let’s
add deleteCourse method to CourseDataService. As you can see it execute the
delete request to specific course api url.
deleteCourse(name,
id) {
//console.log('executed service')
return
axios.delete(`${INSTRUCTOR_API_URL}/courses/${id}`);
}
We can add a delete button corresponding
to each of the courses:
<td><button
className="btn btn-warning" onClick={() =>
this.deleteCourseClicked(course.id)}>Delete</button>
</td>
On click of the button we are calling the deleteCourseClicked method
passing the course id. The implementation for deleteCourseClicked is shown
below:
When we get a
successful response for delete API call, we set a message into state and
refresh the courses list.
deleteCourseClicked(id)
{ CourseDataService.deleteCourse(INSTRUCTOR, id)
.then(
response => {
this.setState({
message: `Delete of course ${id} Successful` }) this.refreshCourses()
}
)
}
We display the message just below the
header
<h3>All
Courses</h3>
{this.state.message && <div class="alert
alert-success">{this.state.message}</div>} Of course - we have to
ensure that the method is bound to this in the constructor.
this.deleteCourseClicked
= this.deleteCourseClicked.bind(this) Complete ListCoursesComponent, at this
stage, is shown below:
class
ListCoursesComponent extends Component { constructor(props) {
super(props)
this.state = { courses: [], message: null
}
}
render()
{ console.log('render') return (
<div className="container">
<h3>All Courses</h3>
{this.state.message && <div
class="alert alert-success">{this.state.message}</div>}
<div
className="container">
<table className="table">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{
this.state.courses.map( course =>
<tr key={course.id}>
<td>{course.id}</td>
<td>{course.description}</td>
<td><button
className="btn btn-warning" onClick={() =>
this.deleteCourseClicked(course.id)}>Delete</button>
</td>
</tr>
)
}
</tbody>
</table>
</div>
</div>
)
}
#Updating
Course Details
To be able to update the course details, we would need to create a
new component to represent the todo form. Let’s start with creating a simple
component.
/src/component/CourseComponent.jsx
class
CourseComponent extends Component { render() {
return (
<h1>Course
Details</h1>
)
}
}
export
default CourseComponent Implementing Routing
When
the user clicks the update course button on the course listing page, we would
want to route to the course page. How do we do it? That’s where Routing comes
into the picture.
/src/component/InstructorApp.jsx
class
InstructorApp extends Component { render() {
return (
<Router>
<>
<h1>Instructor
Application</h1>
<Switch>
<Route path="/" exact
component={ListCoursesComponent} />
<Route
path="/courses" exact component={ListCoursesComponent} />
<Route path="/courses/:id"
component={CourseComponent} />
</Switch>
</>
</Router>
)
}
}
export default
InstructorApp
We are defining a Router around all the
components and configuring paths to each of them.
http://localhost:3000/
takes you to home page http://localhost:3000/courses takes you to course
listing page http://localhost:3000/courses/2 takes you to course page
When you launch the React app in the browser using
this URL http://localhost:3000/courses/2, it will appear as
#Adding
Update Button to Course Listing Page Let’s add update button to the course
listing page.
/src/component/ListCoursesComponent.jsx
.....
<th>Update</th>
....
<td><button className="btn btn-success"
onClick={() =>
this.updateCourseClicked(course.id)}>Update</button></td>
We
can create the add updateCourseClicked method to redirect to Course Component
and add the binding in the constructor method.
...
this.updateCourseClicked =
this.updateCourseClicked.bind(this)
...
updateCourseClicked(id)
{ console.log('update ' + id) this.props.history.push(`/courses/${id}`)
}
Adding Add button to Course Listing Page
Let’s add an Add button at the bottom of
Course Listing Page.
/src/component/ListCoursesComponent.jsx
<div
className="row">
<button className="btn
btn-success" onClick={this.addCourseClicked}>Add</button>
</div>
Let’s add the appropriate binding and the
method to handle click of Add button.
//In
constructor
this.addCourseClicked
= this.addCourseClicked.bind(this) addCourseClicked() {
this.props.history.push(`/courses/-1`)
}
When you launch the React app in the
browser using this URL http://localhost:3000,
#Create API to
Retrieve Specific Course Details
Now that we have the course component beinging rendered on the click
of update button, let’s start focusing on getting the course details from the
REST API.
Let’s add
findById method to CoursesHardcodedService. It retrieves the details of a
specific course based on it.
public
Course findById(long id) { for (Course course: courses) {
if
(course.getId() == id) { return course;
}
}
return null;
}
Let’s add getCourse method to CourseResource class. It exposes the
GET method to get the details of a specific course based on id.
@GetMapping("/instructors/{username}/courses/{id}")
public Course getCourse(@PathVariable String username, @PathVariable
long id) { return courseManagementService.findById(id);
}
#Invoking the
API from Course Component
Let’s add retrieveCourse method to
CourseDataService
retrieveCourse(name,
id) {
return
axios.get(`${INSTRUCTOR_API_URL}/courses/${id}`);
}
#Before we get to it we would need to be able to get the
course id from the URL. In the course details page, we are redirecting to the url /courses/${id}. From the path
parameter, we would want to capture the id. We can use
this.props.match.params.id to get the id from path parameters.
The code listing below shows the updated CourseComponent.
import React, { Component } from 'react'
import { Formik, Form, Field, ErrorMessage }
from 'formik'; import CourseDataService from '../service/CourseDataService';
const INSTRUCTOR = 'in28minutes'
class CourseComponent extends Component { constructor(props) {
super(props)
this.state = {
id:
this.props.match.params.id, description: ''
}
}
componentDidMount()
{ console.log(this.state.id)
//
eslint-disable-next-line if (this.state.id == -1) { return
}
CourseDataService.retrieveCourse(INSTRUCTOR,
this.state.id)
.then(response => this.setState({ description:
response.data.description
}))
}
render() {
let { description, id } = this.state return (
<div>
<h3>Course</h3>
<div>{id}</div>
<div>{description}</div>
</div>
)
}
}
export default
CourseComponent
We are setting the details of the course
into state.
We are initializing state in the constructor.
this.state = {
id: this.props.match.params.id, description: ''
}
In
componentDidMount, we are calling the CourseDataService.retrieveCourse to get
the details for a course. Once we have the details, we are updating the state.
CourseDataService.retrieveCourse(INSTRUCTOR,
this.state.id)
.then(response => this.setState({ description:
response.data.description
}))
We are updating the render method to show the course details from
component state. render() {
let { description,
id } = this.state
return (
<div>
<h3>Course</h3>
<div>{id}</div>
<div>{description}</div>
</div>
)
}
let {
description, id } = this.state is called destructing. This is similar to
writing code shown below.
let
description = this.state.description let id = this.state.id
This is because
of the logic in componentDidMount to not invoke the course API for a new todo.
if
(this.state.id == -1) { return
}
Create a Form using Formik
Now
that we have loaded up the details of a specific course, let’s shift out our
attention to editing them and saving them back to the database.
To
edit course details, we need a form. The most popular form framework with React
is formik. We already added formik to our package.json using the command npm
add formik.
Let’s
now create a simple form using formik. render() {
let {
description, id } = this.state
return (
<div>
<h3>Course</h3>
<div
className="container">
<Formik
initialValues={{ id, description }}
>
{
(props) => (
<Form>
<fieldset
className="form-group">
<label>Id</label>
<Field
className="form-control" type="text" name="id"
disabled />
</fieldset>
<fieldset
className="form-group">
<label>Description</label>
<Field
className="form-control" type="text"
name="description" />
</fieldset>
<button className="btn
btn-success" type="submit">Save</button>
</Form>
)
}
</Formik>
</div>
</div>
)
}
Following are some of the important
details:
let {
description, id } = this.state - Creating local variable using destructuring
<Formik
initialValues={{ id, description }}> - Initialing Formik with the values
loaded from state
<Field className="form-control" type="text"
name="id" disabled /> - Creating a disabled text element for id.
The name of element should match the name in state.
<Field
className="form-control" type="text"
name="description" /> - Creating a text element for description.
The name of element should match the name in state.
<button className="btn btn-success"
type="submit">Save</button> - Adding a submit button.
Adding Handling
of Submit Event
Let’s try to handle the Submit event now.
Let’s create an onSubmit method to log the values onSubmit(values) {
console.log(values);
}
Let’s bind the onSubmit method to this in
the constructor.
this.onSubmit =
this.onSubmit.bind(this)
It’s time to tie up the form with the
onSubmit method. The key snippet is onSubmit={this.onSubmit}.
<Formik
initialValues={{
id, description }} onSubmit={this.onSubmit}
>
When you click Submit, the form details
are now printed to the console.
{id:
"2", description: "Learn Microservices"} Adding Validation
using Formik
What’s
a form without validation? Let’s add a validate method.
validate(values)
{ let errors = {}
if
(!values.description) { errors.description = 'Enter a Description'
} else if (values.description.length < 5) {
errors.description
= 'Enter atleast 5 Characters in Description'
}
return errors
}
We are adding
two validations:
check
for empty description check for a minimum length of 5
You can add other
validations as you need. As usual, let’s bind it to this in the constructor.
this.validate =
this.validate.bind(this)
Let’s ties this up with the form. The key snippet is
validate={this.validate}. We do not want to validate on change of value or on blur of the field. Let’s keep things
simple. enableReinitialize={true} is needed to ensure that we can reload the
form for existing todo.
<Formik
initialValues={{ id, description }} onSubmit={this.onSubmit}
validateOnChange={false}
validateOnBlur={false} validate={this.validate} enableReinitialize={true}>
If
you run the page right now and submit invalid description, you would see that
validations prevent the form from getting submitted.
Formik
provides ErrorMessage. Let’s add error message to the field:
<ErrorMessage
name="description" component="div" className="alert
alert-warning" />
#Updating
Course Details on click of submit
Now that the form is ready, we would want to call the backend API to
save the course details. Let’s quickly create the API to Update and Create
Courses.
Create API to
Update Course
Let’s add a save
method to CoursesHardcodedService to handle creation and updation of course.
public Course
save(Course course) {
if
(course.getId() == -1 || course.getId() == 0) {
course.setId(++idCounter); courses.add(course);
}
else { deleteById(course.getId()); courses.add(course);
}
return course;
}
Let’s
add a method to the Resource class to update the course. We are using PUT
method to update the course. On course updation, we are returning 200 status
with updaated course details in the body.
@PutMapping("/instructors/{username}/courses/{id}")
public ResponseEntity<Course> updateCourse(@PathVariable
String username, @PathVariable long id, @RequestBody Course course) {
Course
courseUpdated = courseManagementService.save(course);
return new
ResponseEntity<Course>(courseUpdated, HttpStatus.OK);
}
Adding API to Create Course
Let’s
add a method to the Resource class to create the course. We are using POST
method to create the course. On course updation, we are returning a status of
CREATED.
@PostMapping("/instructors/{username}/courses")
public ResponseEntity<Void> createCourse(@PathVariable String
username, @RequestBody Course course) { Course createdCourse =
courseManagementService.save(course);
// Location
// Get current resource url
/// {id} URI uri =
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(createdCourse.getId())
.toUri();
return
ResponseEntity.created(uri).build();
}
Invoking Update and Create APIs from
Course Screen
Now that the REST API is ready, let’s
create the frontend methods to call them.
Let’s
create respective methods in the CourseDataService. updateCourse uses a put and
createCourse uses post.
class
CourseDataService { updateCourse(name, id, course) {
return
axios.put(`${INSTRUCTOR_API_URL}/courses/${id}`, course);
}
createCourse(name,
course) {
return
axios.post(`${INSTRUCTOR_API_URL}/courses/`, course);
}
Let’s update the
CourseComponent to invoke the right service on the click of the submit button.
onSubmit(values)
{
let username = INSTRUCTOR
let
course = { id: this.state.id,
description:
values.description, targetDate: values.targetDate
}
if
(this.state.id === -1) { CourseDataService.createCourse(username, course)
.then(() => this.props.history.push('/courses'))
} else {
CourseDataService.updateCourse(username,
this.state.id, course)
.then(() =>
this.props.history.push('/courses'))
}
console.log(values);
}
We
are creating a course object with the updated details and calling the
appropriate method on the CourseDataService. Once the request is successful, we
are redirecting the user to the course listing page using
this.props.history.push('/courses').
#Complete Code
Example
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-
maven/public/index.html
<!DOCTYPE html>
<html
lang="en">
<head>
<meta charset="utf-8" />
<link
rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color"
content="#000000" />
<!--
manifest.json provides metadata used when
your web app is installed on a
user's mobile
device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest"
href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the
tags above.
It will be replaced with the URL of the `public` folder during the
build. Only files inside the `public` folder can be referenced from the HTML.
Unlike
"/favicon.ico" or "favicon.ico",
"%PUBLIC_URL%/favicon.ico" will work correctly both with client-side
routing and a non-root public URL. Learn how to configure a non-root public URL
by running `npm run build`.
-->
<title>My Full Stack Application
with Spring Boot and React</title>
</head>
<body>
<noscript>You need to enable
JavaScript to run this app.</noscript>
<div
id="root"></div>
<!--
This HTML file
is a template.
If you open it directly in the browser,
you will see an empty page.
You can add
webfonts, meta tags, or analytics to this file.
The build step
will place the bundled scripts into the <body> tag.
To begin the
development, run `npm start` or `yarn start`.
To create a production bundle, use `npm
run build` or `yarn build`.
-->
</body>
</html>
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-
maven/public/manifest.json
{
"short_name": "React
App",
"name": "Create React App Sample",
"icons": [
{
"src":
"favicon.ico",
"sizes": "64x64 32x32
24x24 16x16",
"type":
"image/x-icon"
}
],
"start_url":
".", "display": "standalone",
"theme_color":
"#000000", "background_color": "#ffffff"
}
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-maven/src/App.css
@import url(https://unpkg.com/bootstrap@4.1.0/dist/css/bootstrap.min.css)
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-maven/src/index.js
import React from 'react';
import
ReactDOM from 'react-dom'; import './index.css';
import App from './App';
import
* as serviceWorker from './serviceWorker'; ReactDOM.render(<App />,
document.getElementById('root'));
// If you want
your app to work offline and load faster, you can change
// unregister() to register() below. Note
this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-
maven/src/component/ListCoursesComponent.jsx
import React, { Component } from 'react'
import
CourseDataService from '../service/CourseDataService'; const INSTRUCTOR = 'in28minutes'
class
ListCoursesComponent extends Component { constructor(props) {
super(props)
this.state = { courses: [], message: null
}
this.deleteCourseClicked
= this.deleteCourseClicked.bind(this) this.updateCourseClicked =
this.updateCourseClicked.bind(this)
this.addCourseClicked
= this.addCourseClicked.bind(this) this.refreshCourses =
this.refreshCourses.bind(this)
}
componentDidMount()
{ this.refreshCourses();
}
refreshCourses()
{ CourseDataService.retrieveAllCourses(INSTRUCTOR)//HARDCODED
.then(
response => {
//console.log(response);
this.setState({
courses: response.data })
}
)
}
deleteCourseClicked(id)
{ CourseDataService.deleteCourse(INSTRUCTOR, id)
.then(
response => {
this.setState({
message: `Delete of course ${id} Successful` }) this.refreshCourses()
}
)
}
addCourseClicked()
{ this.props.history.push(`/courses/-1`)
}
updateCourseClicked(id)
{ console.log('update ' + id) this.props.history.push(`/courses/${id}`)
}
render()
{ console.log('render') return (
<div className="container">
<h3>All Courses</h3>
{this.state.message
&& <div class="alert alert-success">{this.state.message}</div>}
<div
className="container">
<table className="table">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{
this.state.courses.map( course =>
<tr key={course.id}>
<td>{course.id}</td>
<td>{course.description}</td>
<td><button className="btn
btn-success" onClick={() =>
this.updateCourseClicked(course.id)}>Update</button></td>
<td><button className="btn
btn-warning" onClick={() =>
this.deleteCourseClicked(course.id)}>Delete</button>
</td>
</tr>
)
}
</tbody>
</table>
<div className="row">
<button className="btn
btn-success" onClick={this.addCourseClicked}>Add</button>
</div>
</div>
</div>
)
}
}
export default
ListCoursesComponent
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-
maven/src/component/InstructorApp.jsx
import React, { Component } from 'react';
import ListCoursesComponent from
'./ListCoursesComponent';
import
{ BrowserRouter as Router, Route, Switch } from 'react-router-dom' import
CourseComponent from './CourseComponent';
class
InstructorApp extends Component { render() {
return (
<Router>
<>
<h1>Instructor
Application</h1>
<Switch>
<Route path="/" exact
component={ListCoursesComponent} />
<Route path="/courses" exact
component={ListCoursesComponent} />
<Route
path="/courses/:id" component={CourseComponent} />
</Switch>
</>
</Router>
)
}
}
export default
InstructorApp
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-
maven/src/component/CourseComponent.jsx
import React, { Component } from 'react'
import { Formik, Form, Field, ErrorMessage } from 'formik'; import
CourseDataService from '../service/CourseDataService';
const
INSTRUCTOR = 'in28minutes'
class
CourseComponent extends Component { constructor(props) {
super(props)
this.state = {
id:
this.props.match.params.id, description: ''
}
this.onSubmit
= this.onSubmit.bind(this) this.validate = this.validate.bind(this)
}
componentDidMount()
{ console.log(this.state.id)
//
eslint-disable-next-line if (this.state.id == -1) { return
}
CourseDataService.retrieveCourse(INSTRUCTOR,
this.state.id)
.then(response => this.setState({ description:
response.data.description
}))
}
validate(values)
{ let errors = {}
if
(!values.description) { errors.description = 'Enter a Description'
} else if (values.description.length < 5) {
errors.description
= 'Enter atleast 5 Characters in Description'
}
return errors
}
onSubmit(values)
{
let username =
INSTRUCTOR
let
course = { id: this.state.id,
description: values.description
}
if
(this.state.id === -1) { CourseDataService.createCourse(username, course)
.then(() => this.props.history.push('/courses'))
} else {
CourseDataService.updateCourse(username,
this.state.id, course)
.then(() =>
this.props.history.push('/courses'))
}
console.log(values);
}
render() {
let { description, id } = this.state return (
<div>
<h3>Course</h3>
<div
className="container">
<Formik
initialValues={{ id, description }} onSubmit={this.onSubmit}
validateOnChange={false} validateOnBlur={false} validate={this.validate}
enableReinitialize={true}
>
{
(props) => (
<Form>
<ErrorMessage
name="description" component="div" className="alert
alert-warning" />
<fieldset className="form-group">
<label>Id</label>
<Field
className="form-control" type="text" name="id"
disabled />
</fieldset>
<fieldset
className="form-group">
<label>Description</label>
<Field
className="form-control" type="text"
name="description" />
</fieldset>
<button className="btn
btn-success" type="submit">Save</button>
</Form>
)
}
</Formik>
</div>
</div>
)
}
}
export default
CourseComponent
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-maven/src/index.css
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe
UI", "Roboto", "Oxygen", "Ubuntu",
"Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue",
sans-serif;
-webkit-font-smoothing:
antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier
New", monospace;
}
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-
maven/src/App.test.js
import React from 'react';
import
ReactDOM from 'react-dom'; import App from './App';
it('renders
without crashing', () => {
const
div = document.createElement('div'); ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
/spring-boot-react-crud-full-stack-with-maven/frontend-spring-boot-react-crud-full-stack-with-
maven/src/serviceWorker.js
// This optional code is used to register a service
worker.
// register() is
not called by default.
// This lets
the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it
also means that developers (and users)
// will only see
deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have
been closed, since previously cached
// resources are updated in the
background.
// To learn more
about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const
isLocalhost = Boolean( window.location.hostname === 'localhost' ||
//
[::1] is the IPv6 localhost address. window.location.hostname === '[::1]' ||
//
127.0.0.1/8 is considered localhost for IPv4. window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function
register(config) {
if
(process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator)
{
// The URL constructor is available in
all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL,
window.location.href); if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on
a different origin
// from what our
page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374 return;
}
window.addEventListener('load',
() => {
const swUrl =
`${process.env.PUBLIC_URL}/service-worker.js`;
if
(isLocalhost) {
// This is running on localhost. Let's check if a service worker
still exists or not. checkValidServiceWorker(swUrl, config);
// Add some
additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { console.log(
'This
web app is being served cache-first by a service ' + 'worker. To learn more,
visit https://bit.ly/CRA-PWA'
);
});
} else {
//
Is not localhost. Just register service worker registerValidSW(swUrl, config);
}
});
}
}
function
registerValidSW(swUrl, config) { navigator.serviceWorker
.register(swUrl)
.then(registration => { registration.onupdatefound = () => {
const
installingWorker = registration.installing; if (installingWorker == null) {
return;
}
installingWorker.onstatechange
= () => {
4 Comments
good
ReplyDeleteneed some more information
ReplyDeleteVery Good… i really like your blog…
ReplyDeleteThanks for writing this great article.
ReplyDelete