In this tutorial, we will build React Redux Http Client & Spring Boot Server example that uses Spring Data to interact with Cassandra 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
– Cassandra 3.9.0
Overview
1. Spring Boot Server
Spring Data Cassandra example:
How to start Spring Data Cassandra with SpringBoot
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 CassandraRepository, 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 Cassandra properties in application.properties
– Dependencies for Spring Boot and Spring Data Cassandra in pom.xml
2. React Redux Client
– 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
0. Set up Cassandra
Open Cassandra CQL Shell:
– Create Cassandra keyspace with name javasampleapproach:
create keyspace javasampleapproach with replication={'class':'SimpleStrategy', 'replication_factor':1};
– Create book table for javasampleapproach keyspace:
use javasampleapproach; CREATE TABLE book( id timeuuid PRIMARY KEY, title text, author text, description text, published int );
1. Spring Boot Server
1.1 Dependency
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-cassandra</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
1.2 Book – Data Model
model/Book.java
package com.javasampleapproach.spring.cassandra.model; import java.util.UUID; import org.springframework.data.cassandra.core.mapping.PrimaryKey; import org.springframework.data.cassandra.core.mapping.Table; @Table public class Book { @PrimaryKey private UUID id; private String title; private String author; private String description; private int published; public Book() { } public UUID getId() { return id; } public void setId(UUID id) { this.id = 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 Cassandra Repository
repo/BookRepository.java
package com.javasampleapproach.spring.cassandra.repo; import java.util.UUID; import org.springframework.data.cassandra.repository.CassandraRepository; import com.javasampleapproach.spring.cassandra.model.Book; public interface BookRepository extends CassandraRepository{ }
1.4 REST Controller
controller/BookController.java
package com.javasampleapproach.spring.cassandra.controller; import java.util.List; import java.util.Optional; import java.util.UUID; 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.datastax.driver.core.utils.UUIDs; import com.javasampleapproach.spring.cassandra.model.Book; import com.javasampleapproach.spring.cassandra.repo.BookRepository; @CrossOrigin(origins = "http://localhost:8081") @RestController @RequestMapping("/api") public class BookController { @Autowired BookRepository bookRepository; @GetMapping("/books") public ListgetAllBooks() { System.out.println("Get all Books..."); return bookRepository.findAll(); } @PostMapping("/books/create") public ResponseEntity createBook(@Valid @RequestBody Book book) { System.out.println("Create Book: " + book.getTitle() + "..."); book.setId(UUIDs.timeBased()); Book _book = bookRepository.save(book); return new ResponseEntity<>(_book, HttpStatus.OK); } @PutMapping("/books/{id}") public ResponseEntity updateBook(@PathVariable("id") UUID 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") UUID id) { System.out.println("Delete Baook 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 Data Cassandra
application.properties
spring.data.cassandra.keyspace-name=javasampleapproach spring.data.cassandra.contact-points=127.0.0.1 spring.data.cassandra.port=9042
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> </Link> <p>Author: {author}</p> {description && <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 /> </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} /> </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) => (); export default connect()(AddBook);Set Book information:
{ props.dispatch(addBook(book)); props.history.push('/'); }} />
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('/'); }} /> </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 Cassandra database:
Click on a Book’s title, app goes to Edit Page:
Click Add Book button and check new Book list:
Click on certain Remove button to remove certain Book.
For example, removing Origin:
Check Cassandra Database:
Source Code
– SpringBootCassandra-server
– ReactReduxHttpClient