React Spring Boot & MYSQL - Full Stack Product CRUD Website

 In the earlier tutorials, you learnt to create API endpoints in Spring Boot & MYQL to perform CRUD operationsCRUD operations, upload form data with files, and to setup authentication & role-based access control using JWT

In this tutorial, you learn to create React front-end app to access the API endpoints. After completing this tutorial, you build a simple product admin panel that allows an authorized user to add new products with image files, update and delete existing products from MYSQL database.

In working directory of your choice, execute the following command to setup a React app.

npm init react-app spr-client

After the app initialization completes, the spr-client folder is created. To run the app, change to spr-client folder and execute the command below:

spr-client>npm run start

We use bootstrap to style the app. Let install bootstrap by executing the command below:

spr-client>npm install bootstrap

We use icons and components from react-icons and reactstrap. To install these packages, execute the commands:

spr-client>npm install react-icons reactstrap

In src, create components folder. Add NavBar.js file to the components folder. The NavBar component display a top navigation bar that has two items: Home and Login/Logout items.

components/NavBar.js

import React, { useState } from 'react';

import {
  Collapse,
  Navbar,
  NavbarToggler,
  NavbarBrand,
  Nav,
  NavItem,
  NavLink,
  UncontrolledDropdown,
  DropdownToggle,
  DropdownMenu,
  DropdownItem,

} from 'reactstrap';

function NavBar(props) {
  const {token} = props;
  const [isOpen, setIsOpen] = useState(false);
  const toggle = () => setIsOpen(!isOpen);

  return (
    <div>
    <Navbar color="light" light expand="md">
      <NavbarBrand href="/">Home</NavbarBrand>
      <NavbarToggler onClick={toggle} />
      <Collapse isOpen={isOpen} navbar>
        <Nav navbar>
          <NavItem >
            {
            token?  
            <NavLink href="/logout">

              Logout
             
            </NavLink>
            :
            <NavLink href="/login">

              Login
             
            </NavLink>

           }
          </NavItem>

         
        </Nav>
      </Collapse>
     
    </Navbar>
  </div>
  );
}

export default NavBar;

Add register.js, login.js, logout.js, and product.js files to the components folder.

The register.js contains Register component to display a register form to create a new user account.

components/register.js


import React, { useState } from "react"
import axios from "axios";
import { useNavigate } from "react-router-dom";

import {
  Button,
  FormGroup,Card,
  CardHeader,CardBody,CardFooter,  
  Input,Label,
  Form,Col,
} from "reactstrap";

const Register = (props) => {

      const [errors,setErrors] = useState({});  
      const [inputs, setInputs] = useState({});
      const history=useNavigate();


      function handleValidation() {
        
        let formIsValid = true;
        let es = {};
        //Name
        if (!inputs.username) {
          formIsValid = false;
          es['username']="can not empty!";
         
        }
    
        if (typeof inputs.username !== "undefined") {
            
          if (!inputs.username.match(/^[a-zA-Z0-9]+$/)) {
            formIsValid = false;
            es['username']="only letters and numbers allowed!";
          }
        }
        // password
        if (!inputs.password) {
            formIsValid = false;
            es['password']="can not empty!";
          }
      
          if (typeof inputs.password !== "undefined") {
            if (inputs.password.length<8) {
              formIsValid = false;
              es['password']="week password!";
            }
          }
        //Email
        if (!inputs.email) {
          formIsValid = false;
          es['email']="can not empty!";
        }

        if (typeof inputs.email!== "undefined") {
        
          if (!(/\S+@\S+\.\S+/.test(inputs.email))) {
            formIsValid = false;
            es['email']="invalid email!";
          }
        }
        //password 2
        if (!inputs.password2) {
          formIsValid = false;
          es['password2']="can not empty!";
        }
    
        if (inputs.password!==inputs.password2) {
        
          formIsValid = false;
          es['password2']="Passwords not match!";
          
        }
        setErrors(es);
        return formIsValid;
      }
      const  ROLE_CHOICES = [
        {"id":1,"text":"admin"},
        {"id":2,"text":"mod"},
        {"id":3,"text":"user"},
        
        ];

      function handleSubmit(e){
        e.preventDefault();

        if(handleValidation()){
            axios({
                method: 'post',
      
                url: "/api/auth/register",
                data: {username: inputs.username,password:inputs.password,email:inputs.email,role:[inputs.role?inputs.role:'admin']},
                headers: { "Content-type":"application/json",}
            }).then(response=>{

                    try {
                        console.log(response);
     
                        let dt=JSON.parse(JSON.stringify(response));
                        if(dt.status===200){
                            history("/login");
                        }
                        else{
                            alert('Failed to create user!');
                        }
                        
                    } catch (e) {
                        alert('Failed to create user!');
                    }
                    
                    
                    
            });
        }
      }
      
      const handleChange = (event) =>{
        const name = event.target.name;
        const value = event.target.value;
        setInputs(values => ({...values, [name]: value}))
      }
      return (
        <div className="container"  style={{width: '18rem'}}>
            <Card
                className="my-2" style={{width: '18rem'}}
            >

            <CardHeader>Register</CardHeader>
            
            <Form className="form">
                <CardBody>
                <FormGroup>
                    <Input
                        type="text"
                        name="username"
                        placeholder="Enter username"
                        value={inputs.username || ""} 
                        onChange={handleChange}
                       
                        />
                    <span style={{color: '#ff2222'}}>{errors.username}</span>
                    </FormGroup>
                    <FormGroup>
                    <Input
                        type="email"
                        name="email"
                        placeholder="Enter email"
                        value={inputs.email || ""} 
                        onChange={handleChange}
                       
                        />
                    <span style={{color: '#ff2222'}}>{errors.email}</span>
                    </FormGroup>
                   
                    <FormGroup>

                        <Input
                        type="password"
                        name="password"
                        placeholder="Enter password"
                        value={inputs.password || ""} 
                        onChange={handleChange}
                       
                        />
                    <span style={{color: '#ff2222'}}>{errors.password}</span>
                    </FormGroup>

                    <FormGroup>

                        <Input
                        type="password"
                        name="password2"
                        placeholder="Enter confirm password"
                        value={inputs.password2 || ""} 
                        onChange={handleChange}
                       
                        />
                    <span style={{color: '#ff2222'}}>{errors.password2}</span>
                    </FormGroup>
                    <FormGroup row>
                    <Label for="emp_status"  sm={10}>Role</Label>
                    <Col sm={10}>
                    <Input onChange={(e) =>handleChange(e)} type="select" name="role" id="role" value={inputs.role}>
                          {
                            ROLE_CHOICES.map((emp)=>{
                              return (
                                <option key={emp.id} value={emp.text}>{emp.text}</option>
                              );
                            })
                          }
                    </Input>
                    </Col>
                  </FormGroup>
                </CardBody>   

                <CardFooter>
                    <FormGroup>
        
                    <Button color="primary" onClick={handleSubmit}>Register</Button>
                    </FormGroup>
                </CardFooter>
            </Form>  
            </Card>      
        </div>
      
      )
}
export default Register

The API endpoint to create a new user in MYSQL database can be access via http://localhost:8080/api/auth/register. It is better to use only /api/auth/register. This can be achieved using proxy that will forward API request from port 3000 to 8080. 
Simply update package.json file to add the proxy.
  ........
   "name": "spr-client",
  "version": "0.1.0",
  "private": true,
  "homepage": ".",
  "proxy": "http://localhost:8080",
........

We use axios to make API calls to Spring Boot API endpoints. Thus, you have to install axios into the spr-client app. 

spr-client>npm install axios

The Login component display a login form to authenticate the use using username and password. If the login is successful,  it will receive a token from the server. The token is saved in local storage for subsequent requests to protect API endpoints. The Login component informs its parent component (App.js) by calling the handleTokenChange() method to update login status in NavBar. Finally, the user is redirected to the product admin page (home page).

components/login.js

import React, { useState } from "react"
import axios from "axios";
import { useNavigate } from "react-router-dom";
import { Link } from "react-router-dom"
import {
  Button,
  FormGroup,Card,
  CardHeader,CardBody,CardFooter,  
  Input,
  Form,
  Label,
} from "reactstrap";

const Login = (props) => {
      const [username, setUsername]=useState('');
      const [password,setPassword]=useState('');
      const [logerr,setLogError] = useState('');  
      const {tokenchange} = props;
      const history=useNavigate();
      
      function handleTokenChange(tk){
            tokenchange(tk);
      }

      function handleSubmit(e){
        e.preventDefault();
   
        // Make the POST call to login API endpoint
        axios({
            method: 'post',
            url: "/api/auth/login",
            data: {username: username,password:password},
            headers: { "Content-type":"application/json"}
        }).then(response=>{
                
                console.log("res=",response);
                try {
                    let dt=JSON.parse(JSON.stringify(response));
                    
                    if(dt.status===200){
               
                        localStorage.setItem("token", dt.data.accessToken);
                        handleTokenChange(dt.data.accessToken);
                        history("/");
                    }
                    else{
                        setLogError("Error in login");
                    }
                    
                } catch (e) {
                    setLogError('Invalid user!');
                }
                
                
                
        })
        .catch(error => { 
          
            setLogError('Invalid user!');
        });
      }
      const handleUsernameInputChange = (event) => {
        setUsername(event.target.value);
      }
      const handlePasswordInputChange = (event) => {
        setPassword(event.target.value);
      }
      return (
        <div className="container"  style={{width: '25rem'}}>
            <Card
                className="my-2" style={{width: '25rem'}}
            >

            <CardHeader>Login</CardHeader>
            
            <Form className="form">
                <CardBody>
                    <FormGroup>

                    <Input
                        type="text"
                        name="username"
                        placeholder="Enter username"
                        value={username}
                        onChange={(e) =>handleUsernameInputChange(e)}
                        required
                        />
        
                    </FormGroup>
                    <FormGroup>

                        <Input
                        type="password"
                        name="password"
                        placeholder="Enter password"
                        value={password}
                        onChange={(e) =>handlePasswordInputChange(e)}
                        required
                        />
        
                    </FormGroup>
                </CardBody>   

                <CardFooter>
                    <FormGroup>
        
                    <Button color="primary" onClick={handleSubmit}>Login</Button>
                    <Label style={{marginLeft: '5px'}}>Don't have an account?</Label>  <Link to="/register">create user</Link>
                    <Label className="text-danger">{logerr}</Label>
                    
                    </FormGroup>
                </CardFooter>
            </Form>  
            </Card>      
        </div>
      
      )
}
export default Login

The Logout component simply removes the token from the local storage and informs its parent component (App.js) to change from login to logout state.

components/logout.js
import React,{useEffect} from "react"
import { useNavigate } from "react-router-dom";

const Logout = (props) => {

      const {tokenchange} = props; 
      const history=useNavigate();
      function handleTokenChange (tk) {
          tokenchange(tk);
      }

      useEffect(() => {
        localStorage.removeItem('token');
        handleTokenChange(null);
        history("/");
         }, []);

      return (
        <><div>Logout work</div></>
      
      )
}
export default Logout

The Product component allows an authorized user to manage products. He/she see list of products, add new product with an image file, update and delete existing products from MYSQL database.
components/Product.js
import React, { useState, useEffect } from "react"
import axios from "axios";
import { FaTrash } from "react-icons/fa"
import { FaEdit } from "react-icons/fa"
import { useNavigate } from "react-router-dom";

import { Button, FormGroup,Col, Card, CardHeader,CardBody, Input, Form, Label, Table } from "reactstrap"; const Product= (props) => { const token= localStorage.getItem("token");
  const history=useNavigate();
const [id, setId]=useState(0); const [inputs, setInputs] = useState( { 'id':0, 'name':'', } ); const [fileatt, setDocument] = useState(null); const [isEditMode, setIsEditmode]= useState(false); const [message,setMessage]= useState(''); const [products, setProducts]=useState(null); const [refresh, setRefresh]=useState(false); useEffect(() => { if(token){ axios.get(`/products/all`,{ headers:{ "Content-type":"application/json", "authorization": `Bearer ${token}`, }, }) .then((res) => { const resObj=JSON.parse(JSON.stringify(res)); if(resObj.status===200){ setProducts(resObj.data); } }) .catch((err) => console.log(err)); }else{ console.log('Unauthorized user');
history("/login");
} }, [refresh]); const handleChange = (event) =>{ const name = event.target.name; const value = event.target.value; setInputs(values => ({...values, [name]: value})) } const handleFileChange = (e) => { setDocument(e.target.files[0]); }; const editProduct = (item) =>{ setIsEditmode(true); setId(parseInt(item.id)); setInputs(values => ({...values, ['name']: item.name})); setInputs(values => ({...values, ['price']: item.price}));
setInputs(values => ({...values, ['thumbnail']: item.thumbnail}));
} const deleteProduct = (id) => { axios({ method: 'delete',
      url: "/products/delete/"+id+"/",
      headers: { "Content-type":"application/json",
      "authorization": `Bearer ${token}`}
    }).then(response=>{
      setRefresh(!refresh); 
      
    });
    };  
  
    const handleSubmit = (e) => {
    e.preventDefault();
  
    let form_data = new FormData();
    form_data.append('id', id);
    form_data.append('name', inputs.name);
    form_data.append('price', inputs.price);
    if(inputs.thumbnail) form_data.append('thumbnail', inputs.thumbnail);

if(fileatt!=null) { form_data.append('file', fileatt, fileatt.name); } for (var key of form_data.entries()) { console.log(key[0] + ', ' + key[1]); } // Make the POST call by passing a config object to the instance axios({ method: isEditMode?'put':'post', url: "/products/"+(!isEditMode?"add":"update/"+id), data: form_data, headers: { "authorization": `Bearer ${token}` } }).then(res=>{ const resObj=JSON.parse(JSON.stringify(res)); if(resObj.status===201 || resObj.status===200){ setMessage(" saved successfully!"); } else{ setMessage(" Failed to save!"); } setRefresh(!refresh); }); }; const reset = () =>{ setIsEditmode(false); setId(parseInt(0)); setInputs(values => ({...values, ['name']: ''})); setInputs(values => ({...values, ['price']: 0.0}));
setInputs(values => ({...values, ['thumbnail']: ''}));
} return ( <div className="container" style={{width: '30rem',marginTop:"5px"}}> <Card className="my-2" style={{width: '30rem'}} > <CardHeader>Add/Update Product</CardHeader> <Form> <CardBody> <FormGroup row> <Label for="name" sm={4} size="lg">Name</Label> <Col sm={8}> <Input type="text" value={inputs.name} name="name" id="name" placeholder="Name" bsSize="lg" onChange={(e) =>handleChange(e)} required /> </Col> </FormGroup> <FormGroup row> <Label for="price" sm={4}>Price</Label> <Col sm={8}> <Input type="text" value={inputs.price} name="price" id="price" placeholder="Price" bsSize="lg" onChange={(e) =>handleChange(e)} required /> </Col> </FormGroup> <FormGroup> <Label for="file" sm={10}>Image</Label> <Col sm={10}> <Input type="file" id="fileatt" name="fileatt" accept="image/*" onChange={handleFileChange} /> </Col> </FormGroup> <FormGroup row> <Col sm={3}> <Button color="primary" onClick={handleSubmit}>Submit</Button> </Col> <Col sm={2}> <Button color="primary" onClick={reset}>New</Button> </Col> <Col sm={4}><span>{message}</span></Col> </FormGroup> </CardBody> </Form> </Card> <h1>Products</h1> { <Table striped bordered hover> <thead> <tr><th>Id</th><th>Name</th><th>Price</th><th>Image</th><th>Actions</th></tr> </thead> <tbody> { products && products.map((item, i) => { return ( <tr key={item.id}> <td >{item.id}</td> <td >{item.name}</td> <td >{item.price}</td> <td ><img style={{width: '50px',height:"50px"}} src={"image/"+item.thumbnail}></img></td> <td > <Button color="primary" onClick={() =>editProduct(item)}> <FaEdit style={{ color: "white", fontSize: "12px" }} /> </Button></td> <td > <Button color="primary" onClick={() =>deleteProduct(item.id)}> <FaTrash style={{ color: "red", fontSize: "12px" }} /> </Button></td> </tr> ); }) } </tbody> </Table> } </div> ) } export default Product

Finally, add NavBar, Register, Login, Logout, and Product components to App.js file. 
import React, { useState } from "react"
import "bootstrap/dist/css/bootstrap.min.css";
import NavBar from './components/NavBar';
import Product from './components/Product';
import { BrowserRouter as Router, Routes,Route } from "react-router-dom";
import Register from "./components/register";
import Login from "./components/login";
import Logout from "./components/logout";

function App() {
  const [token,setToken] = useState(localStorage.getItem("token"));
  const handleTokenChange = (tk) =>{
    setToken(tk);
  }
  return (
    <div className="App">
    <Router>
      <NavBar token={token}/>
      <Routes>
      <Route path="/" element ={<Product/>}/>
      <Route path="/login" element ={<Login  tokenchange={handleTokenChange} />}/>
      <Route path="/logout" element ={<Logout  tokenchange={handleTokenChange} />}/>
      <Route path="/register" element ={<Register/>}/>
      </Routes>
    </Router>
  </div>
  );
}

export default App;


Save the project. Then try create a new user with admin role and login to add new products, update and delete products from the list.





Video Demo

Comments