Spring Boot + React Redux + MySQL CRUD example

In this tutorial, we will build React Redux Http Client & Spring Boot Server example that uses Spring Data JPA to interact with MySQL database and React as a front-end technology to make request and receive response.

Related Posts:
How to use Spring JPA with MySQL | Spring Boot
Spring Boot + React Redux + MongoDb CRUD example
Spring Boot + React Redux + Cassandra CRUD example
Spring Boot + React Redux + PostgreSQL CRUD example
How to connect React with Redux

Technologies

– Java 1.8
– Maven 3.3.9
– Spring Tool Suite 3.9.0.RELEASE
– Spring Boot 2.0.1.RELEASE

– Webpack 4.4.1
– React 16.3.0
– Redux 3.7.2
– React Redux 5.0.7
– axios 0.18.0

– MySQL 5.7.16

Overview

react-redux-spring-boot-mysql-crud-example-result-show-books

1. Spring Boot Server

react-redux-spring-boot-mysql-crud-example-spring-server

Spring Data JPA with MySQL example:
How to use Spring JPA with MySQL | Spring Boot

2. React Redux Client

react-redux-spring-boot-mysql-crud-example-react-client

For more details about:
– Redux: A simple practical Redux example
– Middleware: Middleware with Redux Thunk
– Connecting React with Redux: How to connect React with Redux – react-redux example

Practice

Project Structure

1. Spring Boot Server

react-redux-spring-boot-mysql-crud-example-spring-server-structure

– Class Book corresponds to document in books collection.
BookRepository is an interface extends CrudRepository, will be autowired in BookController for implementing repository methods and finder methods.
BookController is a REST Controller which has request mapping methods for RESTful requests such as: getAll, create, update, delete Books.
– Configuration for Spring Data JPA properties in application.properties.
– Dependencies for Spring Boot and Spring Data JPA in pom.xml.

2. React Redux Client

react-redux-spring-boot-mysql-crud-example-react-redux-client-structure

AppRouter is for routing.
actions, reducers and store contains Redux elements.
components folder includes React Components with react-redux connect() function.
axios configures base URL for HTTP client. We use axios methods as async side-effects inside actions/books.js.

How to do

1. Spring Boot Server

1.1 Dependency

pom.xml

<dependency>
	<groupid>org.springframework.boot</groupid>
	<artifactid>spring-boot-starter-web</artifactid>
</dependency>

<dependency>
	<groupid>org.springframework.boot</groupid>
	<artifactid>spring-boot-starter-data-jpa</artifactid>
</dependency>

<dependency>
	<groupid>mysql</groupid>
	<artifactid>mysql-connector-java</artifactid>
	<scope>runtime</scope>
</dependency>
1.2 Book – Data Model

model/Book.java

package com.javasampleapproach.spring.mysql.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "book")
public class Book implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

	@Column(name = "title")
	private String title;

	@Column(name = "author")
	private String author;

	@Column(name = "description")
	private String description;

	@Column(name = "published")
	private int published;

	protected Book() {
	}

	public long getId() {
		return id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public int getPublished() {
		return published;
	}

	public void setPublished(int published) {
		this.published = published;
	}

	@Override
	public String toString() {
		return "Book [id=" + id + ", title=" + title + ", author=" + author + ", description=" + description
				+ ", published=" + published + "]";
	}

}
1.3 Repository

repo/BookRepository.java


package com.javasampleapproach.spring.mysql.repo;

import org.springframework.data.repository.CrudRepository;
import com.javasampleapproach.spring.mysql.model.Book;

public interface BookRepository extends CrudRepository{
}
1.4 REST Controller

controller/BookController.java

package com.javasampleapproach.spring.mysql.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.javasampleapproach.spring.mysql.model.Book;
import com.javasampleapproach.spring.mysql.repo.BookRepository;

@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class BookController {

	@Autowired
	BookRepository bookRepository;

	@GetMapping("/books")
	public List getAllBooks() {
		System.out.println("Get all Books...");

		List list = new ArrayList<>();
		Iterable customers = bookRepository.findAll();

		customers.forEach(list::add);
		return list;
	}

	@PostMapping("/books/create")
	public Book createBook(@Valid @RequestBody Book book) {
		System.out.println("Create Book: " + book.getTitle() + "...");

		return bookRepository.save(book);
	}

	@GetMapping("/books/{id}")
	public ResponseEntity getBook(@PathVariable("id") Long id) {
		System.out.println("Get Book by id...");

		Optional bookData = bookRepository.findById(id);
		if (bookData.isPresent()) {
			return new ResponseEntity<>(bookData.get(), HttpStatus.OK);
		} else {
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
	}

	@PutMapping("/books/{id}")
	public ResponseEntity updateBook(@PathVariable("id") Long id, @RequestBody Book book) {
		System.out.println("Update Book with ID = " + id + "...");

		Optional bookData = bookRepository.findById(id);
		if (bookData.isPresent()) {
			Book savedBook = bookData.get();
			savedBook.setTitle(book.getTitle());
			savedBook.setAuthor(book.getAuthor());
			savedBook.setDescription(book.getDescription());
			savedBook.setPublished(book.getPublished());

			Book updatedBook = bookRepository.save(savedBook);
			return new ResponseEntity<>(updatedBook, HttpStatus.OK);
		} else {
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
	}

	@DeleteMapping("/books/{id}")
	public ResponseEntity deleteBook(@PathVariable("id") Long id) {
		System.out.println("Delete Book with ID = " + id + "...");

		try {
			bookRepository.deleteById(id);
		} catch (Exception e) {
			return new ResponseEntity<>("Fail to delete!", HttpStatus.EXPECTATION_FAILED);
		}

		return new ResponseEntity<>("Book has been deleted!", HttpStatus.OK);
	}
}
1.5 Configuration for Spring Datasource & JPA properties

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.generate-ddl=true

2. React Redux Client

2.1 Dependency

package.json

{
  "name": "react-redux-springboot",
  "version": "1.0.0",
  "main": "index.js",
  "author": "JavaSampleApproach",
  "license": "MIT",
  "scripts": {
    "serve": "live-server public",
    "build": "webpack",
    "dev-server": "webpack-dev-server"
  },
  "dependencies": {
    "babel-cli": "6.24.1",
    "babel-core": "6.25.0",
    "babel-loader": "7.1.4",
    "babel-plugin-transform-object-rest-spread": "6.26.0",
    "babel-preset-env": "1.6.1",
    "babel-preset-react": "6.24.1",
    "css-loader": "0.28.11",
    "node-sass": "4.8.3",
    "react": "16.3.0",
    "react-dom": "16.3.0",
    "react-modal": "3.3.2",
    "react-redux": "5.0.7",
    "react-router-dom": "4.2.2",
    "redux": "3.7.2",
    "sass-loader": "6.0.7",
    "style-loader": "0.20.3",
    "webpack": "4.4.1",
    "webpack-cli": "2.0.13",
    "webpack-dev-server": "3.1.1",
    "redux-thunk": "2.2.0",
    "axios":"0.18.0"
  }
}

.babelrc

{
    "presets": [
        "env",
        "react"
    ],
    "plugins": [
        "transform-object-rest-spread"
    ]
}

Run cmd: yarn install.

2.2 Configure base URL

axios/axios.js

import axios from 'axios';

export default axios.create({
    baseURL: 'http://localhost:8080/api'
});
2.3 Redux Action

actions/books.js


import axios from '../axios/axios';

const _addBook = (book) => ({
    type: 'ADD_BOOK',
    book
});

export const addBook = (bookData = {
    title: '',
    description: '',
    author: '',
    published: 0
}) => {
    return (dispatch) => {
        const book = {
            title: bookData.title,
            description: bookData.description,
            author: bookData.author,
            published: bookData.published
        };

        return axios.post('books/create', book).then(result => {
            dispatch(_addBook(result.data));
        });
    };
};

const _removeBook = ({ id } = {}) => ({
    type: 'REMOVE_BOOK',
    id
});

export const removeBook = ({ id } = {}) => {
    return (dispatch) => {
        return axios.delete(`books/${id}`).then(() => {
            dispatch(_removeBook({ id }));
        })
    }
};

const _editBook = (id, updates) => ({
    type: 'EDIT_BOOK',
    id,
    updates
});

export const editBook = (id, updates) => {
    return (dispatch) => {
        return axios.put(`books/${id}`, updates).then(() => {
            dispatch(_editBook(id, updates));
        });
    }
};

const _getBooks = (books) => ({
    type: 'GET_BOOKs',
    books
});

export const getBooks = () => {
    return (dispatch) => {
        return axios.get('books').then(result => {
            const books = [];

            result.data.forEach(item => {
                books.push(item);
            });

            dispatch(_getBooks(books));
        });
    };
};
2.4 Redux Reducer

reducers/books.js


const booksReducerDefaultState = [];

export default (state = booksReducerDefaultState, action) => {
    switch (action.type) {
        case 'ADD_BOOK':
            return [
                ...state,
                action.book
            ];
        case 'REMOVE_BOOK':
            return state.filter(({ id }) => id !== action.id);
        case 'EDIT_BOOK':
            return state.map((book) => {
                if (book.id === action.id) {
                    return {
                        ...book,
                        ...action.updates
                    };
                } else {
                    return book;
                }
            });
        case 'GET_BOOKs':
            return action.books;
        default:
            return state;
    }
};
2.5 Redux Store

store/store.js


import { createStore, applyMiddleware } from "redux";
import books from '../reducers/books';
import thunk from 'redux-thunk';

export default () => {
    return createStore(books, applyMiddleware(thunk));
};
2.6 React Components

components/Book.js

import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { removeBook } from '../actions/books';
 
const Book = ({ id, title, description, author, published, dispatch }) => (
    <div>
        <link to="{`/book/${id}`}">
            <h4>{title} ({published})</h4>
        
        <p>Author: {author}</p>
        {description &amp;&amp; <p>{description}</p>}
        <button onclick="{()" ==""> {
            dispatch(removeBook({ id }));
        }}>Remove</button>
    </div>
);
 
export default connect()(Book);

components/DashBoard.js

import React from 'react';
import BookList from './BookList';

const DashBoard = () => (
    <div classname="container__list">
        <booklist>
    </booklist></div>
);

export default DashBoard;

components/BookList.js

import React from 'react';
import { connect } from 'react-redux';
import Book from './Book';
 
const BookList = (props) => (
    <div>
        Book List:
        <ul>
            {props.books.map(book => {
                return (
                    <li key="{book.id}">
                        <book {...book}="">
                    </book></li>
                );
            })}
        </ul>
 
    </div>
);
 
const mapStateToProps = (state) => {
    return {
        books: state
    };
}
 
export default connect(mapStateToProps)(BookList);

components/AddBook.js

import React from 'react';
import BookForm from './BookForm';
import { connect } from 'react-redux';
import { addBook } from '../actions/books';
 
const AddBook = (props) => (
    <div>
        <h3>Set Book information:</h3>
        <bookform onsubmitbook="{(book)" ==""> {
                props.dispatch(addBook(book));
                props.history.push('/');
            }}
        />
    </bookform></div>
);
 
export default connect()(AddBook);

components/EditBook.js

import React from 'react';
import BookForm from './BookForm';
import { connect } from 'react-redux';
import { editBook } from '../actions/books';
 
const EditBook = (props) => (
    <div classname="container__box">
        <bookform book="{props.book}" onsubmitbook="{(book)" ==""> {
                props.dispatch(editBook(props.book.id, book));
                props.history.push('/');
            }}
        />
    </bookform></div>
);
 
const mapStateToProps = (state, props) => {
    return {
        book: state.find((book) =>
            book.id === props.match.params.id)
    };
};
 
export default connect(mapStateToProps)(EditBook);

components/BookForm.js

import React from 'react';

export default class BookForm extends React.Component {
    constructor(props) {
        super(props);
        this.onTitleChange = this.onTitleChange.bind(this);
        this.onAuthorChange = this.onAuthorChange.bind(this);
        this.onDescriptionChange = this.onDescriptionChange.bind(this);
        this.onPublishedChange = this.onPublishedChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);

        this.state = {
            title: props.book ? props.book.title : '',
            author: props.book ? props.book.author : '',
            description: props.book ? props.book.description : '',
            published: props.book ? props.book.published : 0,

            error: ''
        };
    }

    onTitleChange(e) {
        const title = e.target.value;
        this.setState(() => ({ title: title }));
    }

    onAuthorChange(e) {
        const author = e.target.value;
        this.setState(() => ({ author: author }));
    }

    onDescriptionChange(e) {
        const description = e.target.value;
        this.setState(() => ({ description: description }));
    }

    onPublishedChange(e) {
        const published = parseInt(e.target.value);
        this.setState(() => ({ published: published }));
    }

    onSubmit(e) {
        e.preventDefault();

        if (!this.state.title || !this.state.author || !this.state.published) {
            this.setState(() => ({ error: 'Please set title & author & published!' }));
        } else {
            this.setState(() => ({ error: '' }));
            this.props.onSubmitBook(
                {
                    title: this.state.title,
                    author: this.state.author,
                    description: this.state.description,
                    published: this.state.published
                }
            );
        }
    }

    render() {
        return (
            <div>
                {this.state.error && <p className='error'>{this.state.error}</p>}
                <form onSubmit={this.onSubmit} className='add-book-form'>

                    <input type="text" placeholder="title" autoFocus
                        value={this.state.title}
                        onChange={this.onTitleChange} />
                    <br />

                    <input type="text" placeholder="author"
                        value={this.state.author}
                        onChange={this.onAuthorChange} />
                    <br />

                    <textarea placeholder="description"
                        value={this.state.description}
                        onChange={this.onDescriptionChange} />
                    <br />

                    <input type="number" placeholder="published"
                        value={this.state.published}
                        onChange={this.onPublishedChange} />
                    <br />
                    <button>Add Book</button>
                </form>
            </div>
        );
    }
}
2.7 React Router
routers/AppRouter.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Header from '../components/Header';
import DashBoard from '../components/DashBoard';
import AddBook from '../components/AddBook';
import EditBook from '../components/EditBook';
import NotFound from '../components/NotFound';

const AppRouter = () => (
    <BrowserRouter>
        <div className='container'>
            <Header />
            <Switch>
                <Route path="/" component={DashBoard} exact={true} />
                <Route path="/add" component={AddBook} />
                <Route path="/book/:id" component={EditBook} />
                <Route component={NotFound} />
            </Switch>
        </div>
    </BrowserRouter>
);

export default AppRouter;
</pre>
<!-- /wp:html -->

<!-- wp:paragraph -->
<p>components/Header.js</p>
<!-- /wp:paragraph -->

<!-- wp:html -->
<pre class="lang:xhtml">
import React from 'react';
import { NavLink } from 'react-router-dom';

const Header = () => (
    <header>
        <h2>Java Sample Approach</h2>
        <h4>Book Mangement Application</h4>
        <div className='header__nav'>
            <NavLink to='/' activeClassName='activeNav' exact={true}>Dashboard</NavLink>
            <NavLink to='/add' activeClassName='activeNav'>Add Book</NavLink>
        </div>
    </header>
);

export default Header;
2.8 Render App
app.js
import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './routers/AppRouter';
import getAppStore from './store/store';
import { getBooks } from './actions/books';
import './styles/styles.scss';

import { Provider } from 'react-redux';

const store = getAppStore();

const template = (
    <Provider store={store}>
        <AppRouter />
    </Provider>
);

store.dispatch(getBooks()).then(() => {
    ReactDOM.render(template, document.getElementById('app'));
});

Run & Check Result

Build and Run Spring Boot project with commandlines: mvn clean install and mvn spring-boot:run.
– Run the React App with command: yarn run dev-server.

– Open browser for url http://localhost:8081/:
Add Book:

react-redux-spring-boot-mysql-crud-example-result-add-book

Show Books:

react-redux-spring-boot-mysql-crud-example-result-show-books

Check MySQL database:

react-redux-spring-boot-mysql-crud-example-result-show-books-database

Click on a Book’s title, app goes to Edit Page:

react-redux-spring-boot-mysql-crud-example-result-edit-book

Click Add Book button and check new Book list:

react-redux-spring-boot-mysql-crud-example-result-edit-book-return

Click on certain Remove button to remove certain Book.
For example, removing Origin:

react-redux-spring-boot-mysql-crud-example-result-remove-book

Check MySQL Database:

react-redux-spring-boot-mysql-crud-example-result-show-books-database-after-edit

Source Code

SpringBootMySQL-server
ReactReduxHttpClient

16 thoughts on “Spring Boot + React Redux + MySQL CRUD example”

  1. This is a great article! Can you please give an example of how one would use react redux in a scenario where there are relationships? IE: with two models : Books and Authors (both with Id’s as the primary Key). How would one implement this ManyToMany relationship in the crud ui? How would we set up the Spring Data Rest post from the react form?

    Any help is greatly appreciated! Thank you.

    Manpreet

  2. when executing the command it gives me an error in the index.js:

    ERROR in ./src/index.js
    Module analysis failed: Unexpected token (7:16)
    You may need a suitable charger to handle this type of file.
    | import registerServiceWorker from ‘./registerServiceWorker’;
    |
    | ReactDOM.render (, document.getElementById (‘root’));
    | registerServiceWorker ();
    |
    @ multi (webpack) -dev-server / client? http: // localhost: 8081 ./src
    i 「wdm」: could not compile.

    1. hi Nikola,

      This is the content pf webpack.config.js:

      const path = require('path');
      
      module.exports = {
          mode: 'development',
          entry: './src/app.js',
          output: {
              path: path.join(__dirname, 'public'),
              filename: 'bundle.js'
          },
          module: {
              rules: [
                  {
                      loader: 'babel-loader',
                      test: /\.js$/,
                      exclude: /node_modules/
                  },
                  {
                      test: /\.s?css$/,
                      use: [
                          'style-loader',
                          'css-loader',
                          'sass-loader'
                      ]
                  }
              ]
          },
          devtool: 'cheap-module-eval-source-map',
          devServer: {
              contentBase: path.join(__dirname, 'public'),
              historyApiFallback: true
          }
      };
      

      You can also find it in the Source Code session of the post.

      Regards,
      ozenero.

      1. change

        const mapStateToProps = (state, props) => {
            return {
                book: state.find((book) =>
                    book.id === props.match.params.id)
        
            };
        };
        

        to

        const mapStateToProps = (state, props) => {
            return {
                book: state.find((book) =>
                    book.id == props.match.params.id)
        
            };
        };
        

        this work for me, hope it helps you.

  3. Every time I run it it says Uncaught Error: Module build failed: Error: Cannot find module ‘node-sass’

Leave a Reply

Your email address will not be published. Required fields are marked *