In this tutorial, we’re gonna build a React CRUD (Create, Read, Update, Delete) Application with JWT Authentication, File upload/ download, and consume CRUD Restful APIs developed by Spring boot.
- JWT Authentication Flow for User Signup & User Login.
- Project Structure for React JWT Authentication (without Redux) with LocalStorage, React Router, Axios, react-bootstrap, react-hook-form.
- Creating React Components with Form Validation.
- Dynamic Navigation Bar in React App.
Let's start with creating a React App using create-react-app CLI.
To create a new app, you may choose one of the following methods:
Using npx
npx create-react-app react-appname
Using npm
npm init react-app react-appname
Install some npm packages:
npm install bootstrap reactstrap
npm install react-bootstrap bootstrap
npm install reactstrap react react-dom
npm install axios
npm install react-router-dom
npm install react-hook-form
------------------------------packege.json--------------------------------
{
"name": "react-hooks-jwt-auth",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.26.1",
"bootstrap": "^4.6.2",
"react": "^17.0.2",
"react-bootstrap": "^2.8.0",
"react-dom": "^17.0.2",
"react-hook-form": "^7.45.1",
"react-router-dom": "^6.14.1",
"react-scripts": "4.0.3",
"react-validation": "^3.0.7",
"reactstrap": "^9.2.0",
"validator": "^13.0.0"
},
"scripts": {
"start": "react-scripts --openssl-legacy-provider start",
"build": "react-scripts --openssl-legacy-provider build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
------------------------------app.js--------------------------------
import React, { useState, useEffect } from "react";
import { Routes, Route, Link } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";
import AuthService from "./services/auth.service";
import Login from "./components/Login";
import Register from "./components/Register";
import Home from "./components/Home";
import Profile from "./components/Profile";
import BoardUser from "./components/BoardUser";
import BoardModerator from "./components/BoardModerator";
import BoardAdmin from "./components/BoardAdmin";
import FormSubmit from "./components/FormSubmit";
import EventBus from "./common/EventBus";
import FormDataFetch from "./components/FormDataFetch";
import FormDtailView from "./components/FormDetailView";
import FormDetailEdit from "./components/FormDetailEdit";
const App = () => {
const [showModeratorBoard, setShowModeratorBoard] = useState(false);
const [showAdminBoard, setShowAdminBoard] = useState(false);
const [currentUser, setCurrentUser] = useState(undefined);
useEffect(() => {
const user = AuthService.getCurrentUser();
if (user) {
setCurrentUser(user);
setShowModeratorBoard(user.roles.includes("ROLE_MODERATOR"));
setShowAdminBoard(user.roles.includes("ROLE_ADMIN"));
}
EventBus.on("logout", () => {
logOut();
});
return () => {
EventBus.remove("logout");
};
}, []);
const logOut = () => {
AuthService.logout();
setShowModeratorBoard(false);
setShowAdminBoard(false);
setCurrentUser(undefined);
};
return (
<div>
<nav className="navbar navbar-expand navbar-dark bg-dark">
<Link to={"/"} className="navbar-brand">
Ramsis Code
</Link>
<div className="navbar-nav mr-auto">
<li className="nav-item">
<Link to={"/home"} className="nav-link">
Home
</Link>
</li>
{showModeratorBoard && (
<li className="nav-item">
<Link to={"/mod"} className="nav-link">
Moderator Board
</Link>
</li>
)}
{showAdminBoard && (
<li className="nav-item">
<Link to={"/admin"} className="nav-link">
Admin Board
</Link>
</li>
)}
{currentUser && (
<li className="nav-item">
<Link to={"/user"} className="nav-link">
User
</Link>
</li>
)}
{currentUser && (
<li className="nav-item">
<Link to={"/formDataFetch"} className="nav-link">
Record Fetch
</Link>
</li>
)}
</div>
{currentUser ? (
<div className="navbar-nav ml-auto">
<li className="nav-item">
<Link to={"/profile"} className="nav-link">
{currentUser.username}
</Link>
</li>
<li className="nav-item">
<a href="/login" className="nav-link" onClick={logOut}>
LogOut
</a>
</li>
</div>
) : (
<div className="navbar-nav ml-auto">
<li className="nav-item">
<Link to={"/login"} className="nav-link">
Login
</Link>
</li>
<li className="nav-item">
<Link to={"/register"} className="nav-link">
Sign Up
</Link>
</li>
</div>
)}
</nav>
<div className="container mt-3">
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/home" element={<Home/>} />
<Route path="/login" element={<Login/>} />
<Route path="/register" element={<Register/>} />
<Route path="/profile" element={<Profile/>} />
<Route path="/user" element={<BoardUser/>} />
<Route path="/mod" element={<BoardModerator/>} />
<Route path="/admin" element={<BoardAdmin/>} />
<Route path="/formDataFetch" element={<FormDataFetch/>} />
<Route path="/formFill" element={<FormSubmit/>} />
<Route path="/formFill" element={<FormSubmit/>} />
<Route path="/formDetailView/:id" element={<FormDtailView/>} />
<Route path="/formDetailEdit/:id" element={<FormDetailEdit/>} />
</Routes>
</div>
</div>
);
};
export default App;
-------------------------------------auth.service.js----------------------------------------
import axios from "axios";
const API_URL = "http://localhost:8080/api/auth/";
const register = (username, email, password) => {
return axios.post(API_URL + "signup", {
username,
email,
password,
});
};
const login = (username, password) => {
alert(username +" "+password);
return axios
.post(API_URL + "signin", {
username,
password,
})
.then((response) => {
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
localStorage.setItem("facilityId", "NHA12039");
localStorage.setItem("facilityName", "Kalesh Hospital Delhi");
localStorage.setItem("Role", "Hospital");
}
return response.data;
});
};
const logout = () => {
localStorage.removeItem("user");
};
const getCurrentUser = () => {
return JSON.parse(localStorage.getItem("user"));
};
const AuthService = {
register,
login,
logout,
getCurrentUser,
};
export default AuthService;
------------------------------------------ auth-header.js---------------------------------------
export default function authHeader() {
const user = JSON.parse(localStorage.getItem('user'));
if (user && user.accessToken) {
return { Authorization: 'Bearer ' + user.accessToken }; // for Spring Boot back-end
// return { 'x-access-token': user.accessToken }; // for Node.js Express back-end
} else {
return {};
}
}
-------------------------------------user.service.js--------------------------------------
import axios from "axios";
import authHeader from "./auth-header";
const API_URL = "http://localhost:8080/api/test/";
const APP_URL = "http://localhost:8080/api/app/";
const getPublicContent = () => {
return axios.get(API_URL + "all");
};
const getUserBoard = () => {
return axios.get(API_URL + "user", { headers: authHeader() });
};
const getModeratorBoard = () => {
return axios.get(API_URL + "mod", { headers: authHeader() });
};
const getAdminBoard = () => {
return axios.get(API_URL + "admin", { headers: authHeader() });
};
const getFormData = () => {
return axios.get(API_URL + "formData", { headers: authHeader() });
};
const getFormDataList = () => {
return axios.get(APP_URL + "getFormData", { headers: authHeader() });
};
const getFormDataById = (id) => {
return axios.get(APP_URL + "getFormDataById/"+id, { headers: authHeader() });
};
const saveForm = (userObject) =>{
//console.log(userObject.name);
const user = JSON.parse(localStorage.getItem('user'));
return axios.post(APP_URL +"formInsert",userObject,
{ headers:{
"Content-Type": "multipart/form-data",
"Authorization" : 'Bearer '+user.accessToken,
}}
);
}
const updateForm = (userObject,id) =>{
const user = JSON.parse(localStorage.getItem('user'));
return axios.put(APP_URL +"formUpdate/"+id,userObject,
{ headers:{
"Content-Type": "multipart/form-data",
"Authorization" : 'Bearer '+user.accessToken,
}}
);
}
const deleteUser = (id) => {
return axios.delete(APP_URL + "deleteUser/"+id, { headers: authHeader() });
};
const getFile = () => {
return axios.get(APP_URL + "files", { headers: authHeader() });
};
const UserService = {
getPublicContent,
getUserBoard,
getModeratorBoard,
getAdminBoard,
getFormData,
saveForm,
getFormDataList,
getFormDataById,
updateForm,
deleteUser,
getFile,
APP_URL,
};
export default UserService;
--------------------------------- FormDetailFetch.js------------------------------
import { useState, useEffect} from "react";
import { Routes, Route, Link } from "react-router-dom";
import FormSubmit from "./FormSubmit";
import UserService from "../services/user.service";
import Table from 'react-bootstrap/Table';
import EventBus from "../common/EventBus";
import FormDtailView from "./FormDetailView";
import FormDetailEdit from "./FormDetailEdit";
const FormDataFetch = () => {
const [users, setUsers] = useState([]);
const [message,setMessage] = useState("");
const userList = () =>{
UserService.getFormDataList().then(
(response) => {
console.log(response.data);
setUsers(response.data);
},
(error) => {
if (error.response===401 || error.response.data.status === 401) {
EventBus.dispatch("logout");
}
}
);
}
useEffect(()=> {
userList();
},[])
const deleteUser = async(id) => {
const response = await UserService.deleteUser(id)
userList();
setMessage(response.data.message+" user id :"+id);
}
return<>
<Link to={"/formFill"} className="badge badge-primary">
Form Registration
</Link>
<div className="container">
<h5>User List</h5>
{message &&(<div className="alert alert-success">
{message}
</div>)}
<Table striped bordered hover>
<thead>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Department</th>
<th>Phone</th>
<th>City</th>
<th>Download</th>
<th>Image</th>
<th>View</th>
<th>Edit</th>
<th>Delete</th>
</thead>
<tbody>
{
users.map((user,index)=>(
<tr>
<td>{index+1}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.department}</td>
<td>{user.phone}</td>
<td>{user.city}</td>
<td>
<a href={user.url}>{user.fileName}</a>
</td>
<td><img src={user.url} width="60" height="40" alt={user.fileName}></img></td>
<td><Link className="btn btn-primary" to={"/formDetailView/"+user.id}>view</Link></td>
<td><Link className="btn btn-primary" to={"/formDetailEdit/"+user.id}>Edit</Link></td>
<td><Link className="btn btn-danger" onClick={()=>deleteUser(user.id)}>Delete</Link></td>
</tr>
))
}
</tbody>
</Table>
<div className="container mt-3">
<Routes>
<Route path="/formFill" element={<FormSubmit/>} />
<Route path="/formDetailView/:id" element={<FormDtailView/>} />
<Route path="/formDetailEdit/:id" element={<FormDetailEdit/>} />
</Routes>
</div>
</div>
</>
}
export default FormDataFetch;
-----------------------------------FormSubmit.js-------------------------------
import React, { useState, useEffect } from "react";
import UserService from "../services/user.service";
import EventBus from "../common/EventBus";
import Form from 'react-bootstrap/Form';
const FormSubmit = () => {
const [content, setContent] = useState("");
useEffect(() => {
UserService.getFormData().then(
(response) => {
setContent(response.data);
},
(error) => {
const _content =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
setContent(_content);
if (error.response && error.response.status === 401) {
EventBus.dispatch("logout");
}
}
);
}, []);
const onChangePicture = e => {
console.log('selectFile: ', selectFile);
setSelectFile(e.target.files[0]);
};
const [name,setName] = useState("");
const [email,setEmail] = useState("");
const [department,setDepartment] = useState("");
const [phone,setPhone] = useState("");
const [city,setCity] = useState("");
const [selectFile, setSelectFile] = useState("");
const [msg, setMessage] = useState("");
const [msgName, setMsgName] = useState("");
const [msgEmail, setMsgEmail] = useState("");
const [msgDept, setMsgDept] = useState("");
const [msgCity, setMsgCity] = useState("");
const [msgPhone, setMsgPhone] = useState("");
const [msgFile, setMsgFile] = useState("");
const validate = () => {
let returnError=false;
const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
if(name===''){
setMsgName("Name is Required.");
returnError=true;
}else{
setMsgName('');
returnError=false;
}
if(email===''){
setMsgEmail("Email is Required.");
returnError=true;
}else if(!regexEmail.test(email)){
setMsgEmail("Invalid Email.");
returnError=true;
} else{
setMsgEmail('');
returnError=false;
}
if(department===''){
setMsgDept("Department is Required.");
returnError=true;
}else{
setMsgDept('');
returnError=false;
}
if(phone===''){
setMsgPhone("Phone is Required.");
returnError=true;
}else{
setMsgPhone('');
returnError=false;
}
if(city===''){
setMsgCity("City is Required.");
returnError=true;
}else{
setMsgCity('');
returnError=false;
}
if(selectFile===''){
setMsgFile("File is Required.");
returnError=true;
}else{
setMsgFile('');
returnError=false;
}
return returnError;
}
const submitForm = (e) =>{
e.preventDefault();
const returnError = validate();
if(returnError===false){
const userObject = new FormData();
userObject.append("name",name);
userObject.append("email",email);
userObject.append("department",department);
userObject.append("phone",phone);
userObject.append("city",city);
userObject.append("selectFile",selectFile);
UserService.saveForm(userObject)
.then((response)=>{
if(response.status===200){
// setMessage(response.data.message);
alert(response.data.message);
setName('');
setEmail('');
setDepartment('');
setPhone('');
setCity('');
setSelectFile('');
}
})
.catch((error)=>{
console.log(error);
setMessage(error.response.data.message);
})
}
}
return (
<div className="container">
<div className="row"></div>
<header className="jumbotron">
<Form onSubmit={submitForm}>
<h5>{content}</h5>
{msg &&(<div className="alert alert-success">
{msg}
</div>)}
<div className="form-group">
<label className="form-label">Name</label>
<input type="text" placeholder="name" value={name} onChange={(e)=> setName(e.target.value)} />
{msgName && <p style={{color: "red"}}>{msgName}</p>}
</div>
<div className="form-group">
<label className="form-label">Email address</label>
<input type="email" placeholder="name@example.com" value={email}
onChange={(e)=> setEmail(e.target.value)} />
{msgEmail && <p style={{color: "red"}}>{msgEmail}</p>}
</div>
<div className="form-group">
<label className="form-label">Department</label>
<input type="text" placeholder="Department" value={department}
onChange={(e)=> setDepartment(e.target.value)} />
{msgDept && <p style={{color: "red"}}>{msgDept}</p>}
</div>
<div className="form-group">
<label className="form-label">Phone</label>
<input type="text" placeholder="phone" value={phone} onChange={(e)=> setPhone(e.target.value)} />
{msgPhone && <p style={{color: "red"}}>{msgPhone}</p>}
</div>
<div className="form-group">
<label className="form-label">City</label>
<input type="text" placeholder="city" value={city} onChange={(e)=> setCity(e.target.value)} />
{msgCity && <p style={{color: "red"}}>{msgCity}</p>}
</div>
<div className="form-group">
<label className="form-label">Upload</label>
<input type="file" onChange={onChangePicture} />
{msgFile && <p style={{color: "red"}}>{msgFile}</p>}
</div>
<div className="form-group">
<button className="btn btn-primary">Submit</button>
</div>
</Form>
</header>
</div>
);
};
export default FormSubmit;
---------------------------------FormDetailEdit.js------------------------------------
import React, { useState, useEffect } from "react";
import UserService from "../services/user.service";
import Form from 'react-bootstrap/Form';
import { useParams } from "react-router-dom";
import { useForm } from "react-hook-form";
const FormDetailEdit = () =>{
const {register,handleSubmit,formState: { errors },} = useForm();
const {id} = useParams();
const [name,setName] = useState("");
const [email,setEmail] = useState("");
const [department,setDepartment] = useState("");
const [phone,setPhone] = useState("");
const [city,setCity] = useState("");
const [message,setMessage] = useState("");
const [selectFile, setSelectFile] = useState("");
const loadUserListById= ()=>{
UserService.getFormDataById(id).then(
(response) => {
const {name,department,email,phone,city} = response.data;
setName(name);
setEmail(email);
setDepartment(department);
setPhone(phone);
setCity(city);
console.log(email);
},
(error) => {
setMessage(error.response.data.message);
}
);
}
useEffect(()=> {
loadUserListById();
},[])
const updateForm = (e) =>{
// e.preventDefault();
const userObject = new FormData();
userObject.append("name",name);
userObject.append("email",email);
userObject.append("department",department);
userObject.append("phone",phone);
userObject.append("city",city);
userObject.append("selectFile",selectFile);
UserService.updateForm(userObject,id)
.then((response)=>{
if(response.status===200){
setMessage(response.data.message);
}
})
.catch((error)=>{
console.log(error);
setMessage(error.response.data.message);
})
}
const onChangePicture = e => {
console.log('selectFile: ', selectFile);
setSelectFile(e.target.files[0]);
};
return <>
<div className="container">
<div className="row"></div>
<header className="jumbotron">
<h5>User Edit</h5>
{message &&(<div className="alert alert-success">
{message}
</div>)}
<Form onSubmit={handleSubmit (updateForm)}>
<div className="form-group">
<label className="form-label">Name</label>
<input {...register("name", { required: "Name is required" })}
placeholder="name" value={name} type="text" onChange={(e)=> setName(e.target.value)} />
{errors.name?.message && <p style={{color: "red"}}>{errors.name?.message} </p>}
</div>
<div className="form-group">
<label className="form-label">Email address</label>
<input {...register("email", { required: "Email is required" ,
pattern:{value: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i, message: "Invalid email." }})}
type="text" placeholder="name@example.com" value={email} onChange={(e)=>
setEmail(e.target.value)} />
{errors.email?.message && <p style={{color: "red"}}>{errors.email?.message}</p>}
</div>
<div className="form-group">
<label className="form-label">Department</label>
<input {...register("department", { required: "Department is required" })}
type="text" placeholder="Department" value={department} onChange={(e)=> setDepartment(e.target.value)} />
{errors.department?.message && <p style={{color: "red"}}>{errors.department?.message} </p>}
</div>
<div className="form-group">
<label className="form-label">Phone</label>
<input {...register("phone", { required: "Phone is required" })}
type="text" placeholder="phone" value={phone} onChange={(e)=> setPhone(e.target.value)} />
{errors.phone?.message && <p style={{color: "red"}}>{errors.phone?.message} </p>}
</div>
<div className="form-group">
<label className="form-label">City</label>
<input {...register("city", { required: "City is required" })} type="text"
placeholder="city" value={city} onChange={(e)=> setCity(e.target.value)} />
{errors.city?.message && <p style={{color: "red"}}>{errors.city?.message} </p>}
</div>
<div className="form-group">
<label className="form-label">Upload</label>
<input {...register("selectFile", { required: "File is required" })}
type="file" onChange={onChangePicture} />
{errors.selectFile?.message && <p style={{color: "red"}}>
{errors.selectFile?.message} </p>}
</div>
<div className="form-group">
<button className="btn btn-primary">Update</button>
</div>
</Form>
</header>
</div>
</>
}
export default FormDetailEdit;
--------------------------------FormDetailView.js-------------------------------
import { useState, useEffect} from "react";
import { useParams } from "react-router-dom";
import UserService from "../services/user.service";
import EventBus from "../common/EventBus";
const FormDtailView = () => {
const {id} = useParams();
const [name,setName] = useState("");
const [email,setEmail] = useState("");
const [department,setDepartment] = useState("");
const [phone,setPhone] = useState("");
const [city,setCity] = useState("");
const [message,setMessage] = useState("");
const fileUrl = UserService.APP_URL+"files/"+id;
const loadUserListById= ()=>{
UserService.getFormDataById(id).then(
(response) => {
const {name,department,email,phone,city} = response.data;
setName(name);
setEmail(email);
setDepartment(department);
setPhone(phone);
setCity(city);
console.log(email);
},
(error) => {
setMessage(error.response.data.message);
if (error.response===401 || error.response.data.status === 401) {
EventBus.dispatch("logout");
}
}
);
}
useEffect(()=> {
loadUserListById();
},[])
return <>
<h2>Detail View</h2>
<h5>{message}</h5>
Name : {name} <br/>
Email :{email} <br/>
Department :{department} <br/>
Phone :{phone} <br/>
City :{city} <br/><br/>
<div>
<img src={fileUrl} height="200" width="200"></img>
</div>
</>
}
export default FormDtailView;
------------------------------Register.js-------------------------------
import React, { useState, useRef } from "react";
import Form from "react-validation/build/form";
import Input from "react-validation/build/input";
import CheckButton from "react-validation/build/button";
import { isEmail } from "validator";
import AuthService from "../services/auth.service";
const required = (value) => {
if (!value) {
return (
<div className="alert alert-danger" role="alert">
This field is required!
</div>
);
}
};
const validEmail = (value) => {
if (!isEmail(value)) {
return (
<div className="alert alert-danger" role="alert">
This is not a valid email.
</div>
);
}
};
const vusername = (value) => {
if (value.length < 3 || value.length > 20) {
return (
<div className="alert alert-danger" role="alert">
The username must be between 3 and 20 characters.
</div>
);
}
};
const vpassword = (value) => {
if (value.length < 6 || value.length > 40) {
return (
<div className="alert alert-danger" role="alert">
The password must be between 6 and 40 characters.
</div>
);
}
};
const Register = () => {
const form = useRef();
const checkBtn = useRef();
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [successful, setSuccessful] = useState(false);
const [message, setMessage] = useState("");
const onChangeUsername = (e) => {
const username = e.target.value;
setUsername(username);
};
const onChangeEmail = (e) => {
const email = e.target.value;
setEmail(email);
};
const onChangePassword = (e) => {
const password = e.target.value;
setPassword(password);
};
const handleRegister = (e) => {
e.preventDefault();
setMessage("");
setSuccessful(false);
form.current.validateAll();
if (checkBtn.current.context._errors.length === 0) {
AuthService.register(username, email, password).then(
(response) => {
setMessage(response.data.message);
setSuccessful(true);
},
(error) => {
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
setMessage(resMessage);
setSuccessful(false);
}
);
}
};
return (
<div className="col-md-12">
<div className="card card-container">
<img
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
alt="profile-img"
className="profile-img-card"
/>
<Form onSubmit={handleRegister} ref={form}>
{!successful && (
<div>
<div className="form-group">
<label htmlFor="username">Username</label>
<Input
type="text"
className="form-control"
name="username"
value={username}
onChange={onChangeUsername}
validations={[required, vusername]}
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<Input
type="text"
className="form-control"
name="email"
value={email}
onChange={onChangeEmail}
validations={[required, validEmail]}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Input
type="password"
className="form-control"
name="password"
value={password}
onChange={onChangePassword}
validations={[required, vpassword]}
/>
</div>
<div className="form-group">
<button className="btn btn-primary btn-block">Sign Up</button>
</div>
</div>
)}
{message && (
<div className="form-group">
<div
className={
successful ? "alert alert-success" : "alert alert-danger"
}
role="alert"
>
{message}
</div>
</div>
)}
<CheckButton style={{ display: "none" }} ref={checkBtn} />
</Form>
</div>
</div>
);
};
export default Register;
Spring Boot API