Spring Boot + React Redux + PostgreSQL CRUD example

springboot react redux postgresql

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

Related Post: How to use Spring JPA with PostgreSQL | Spring Boot

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

– PostgreSQL 9.5.3

Overview

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

1. Spring Boot Server

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

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

2. React Redux Client

react-redux-spring-boot-postgresql-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-postgresql-crud-example-react-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-postgresql-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>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
	<scope>runtime</scope>
</dependency>
1.2 Book – Data Model

model/Book.java


package com.javasampleapproach.spring.postgresql.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.postgresql.repo;

import org.springframework.data.repository.CrudRepository;

import com.javasampleapproach.spring.postgresql.model.Book;

public interface BookRepository extends CrudRepository {

}
1.4 REST Controller

controller/BookController.java


package com.javasampleapproach.spring.postgresql.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.postgresql.model.Book;
import com.javasampleapproach.spring.postgresql.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:postgresql://localhost/testdb
spring.datasource.username=postgres
spring.datasource.password=123
spring.jpa.generate-ddl=true

spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=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>
        </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) => (
    <div>
        <h3>Set Book information:</h3>
        <BookForm
            onSubmitBook={(book) => {
                props.dispatch(addBook(book));
                props.history.push('/');
            }}
        />
    </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('/');
            }}
        />
    </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:

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

Show Books:

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

Check PostgreSQL database:

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

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

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

Click Add Book button and check new Book list:

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

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

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

Check PostgreSQL Database:

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

Source Code

SpringBootPostgreSQL-server
ReactReduxHttpClient

4 thoughts on “Spring Boot + React Redux + PostgreSQL CRUD example”

  1. Do you have a spam problem on this site; I also am a blogger, and I
    was wanting to know your situation; we have created some nice practices and we are looking to exchange solutions with other
    folks, why not shoot me an email if interested.

  2. Right here is the perfect website for anybody who hopes to
    understand this topic. You realize so much its almost tough to argue with you (not that I really will need to…HaHa).
    You definitely put a fresh spin on a topic that’s been discussed for a long time.
    Wonderful stuff, just wonderful!

  3. Instagram Follower Kaufen
    Instagram-Follower kaufen: mehr Trust und Reichweite für Ihre Brand
    Das Ziel, die Reichweite auf Instagram zu erhöhen, ist für viele Unternehmen und Influencer häufig nur schwer
    zu erreichen. Follower zu kaufen ist eine bewährte Methode, um die Instagram-Präsenz aufzubessern und mehr Menschen und
    potenzielle Kunden im beliebten sozialen Netzwerk zu erreichen.
    Instagram Follower Kaufen

Leave a Reply

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