This article 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.
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

1. Spring Boot Server

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

2. React Redux 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

– 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: getAllcreateupdatedelete 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

– AppRouter is for routing.
– actionsreducers 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() {
  }
..
}
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<Book, Long>{
}
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<book> getAllBooks() {
    System.out.println("Get all Books...");

    List<book> list = new ArrayList<>();
    Iterable<book> 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<book> getBook(@PathVariable("id") Long id) {
    System.out.println("Get Book by id...");

    Optional<book> 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<book> updateBook(@PathVariable("id") Long id,
@RequestBody Book book) {
    System.out.println("Update Book with ID = " + id + "...");

    Optional<book> 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<string> 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
..
.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;

components/Header.js
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:


Show Books:


Check MySQL database:

Source Code

– SpringBootMySQL-server
– ReactReduxHttpClient


By grokonez | May 3, 2018