Spring Boot + ReactJS: React Table Pagination Example
Hello everyone, Today we will learn how to develop a simple Spring Boot, React, Data table Pagination application.
Technologies Used:
Backend:
- Java 17
- Spring Boot 2.7.0
- Spring Data JPA
- H2 Database
Frontend:
- React 17.0.1
- Axios 0.27.2
- Bootstrap 4.6.0
- React table 7.8.0
Features:
- Add User
- Update User
- Delete User
- View User
- Search users by country
- Server-side pagination
Backend Project Directory:
We will build two projects:
1. Backend: springboot-pagination
2. Frontend: react-datatable-pagination
Project 1: springboot-pagination
pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.knf.dev.demo</groupId> <artifactId>springboot-pagination</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-pagination</name> <description>Demo project for Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
User.java
@Entity@Table(name = "userData")public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name") private String name; @Column(name = "country") private String country; @Column(name = "email") private String email;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public User(Long id, String name, String country, String email) { super(); this.id = id; this.name = name; this.country = country; this.email = email; }
public User() { super(); }}
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByCountryContaining (String country, Pageable pageable);
}
ResourceNotFoundException.java
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String message) { super(message); }}
ServerError.java
public class ServerError extends RuntimeException {
private static final long serialVersionUID = 1L;
public ServerError(String message) { super(message); }
}
GlobalExceptionHandler.java
@ControllerAdvicepublic class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<String> resourceNotFound(Exception e, WebRequest request)
{ return new ResponseEntity<String> (e.getMessage(), HttpStatus.NOT_FOUND); }}
UserController.java
@CrossOrigin(origins = "*")@RestController@RequestMapping("/api/v1/")public class UserController {
@Autowired UserRepository userRepsoitory;
@GetMapping("/users") public Map<String, Object> getAllUsers( @RequestParam(value = "country", required = false) String country, @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "4") int size) { try {
List<User> users = new ArrayList<User>(); Pageable pagination = PageRequest.of(page, size); Page<User> userPage; if (country == null) { userPage = userRepsoitory.findAll(pagination); } else { userPage = userRepsoitory .findByCountryContaining(country, pagination); } users = userPage.getContent(); Map<String, Object> response = new HashMap<String, Object>(); response.put("users", users); response.put("totalPages", userPage.getTotalPages()); return response; } catch (Exception e) { throw new ServerError(e.getMessage()); }
}
@PostMapping("/users") public User addUser(@RequestBody User user) {
return userRepsoitory.save(user); }
@PutMapping("/users/{id}") public User updateUser(@PathVariable("id") Long id, @RequestBody User user) {
User userDetails = userRepsoitory.findById(id) .orElseThrow(() -> new ResourceNotFoundException ("User Not Found")); userDetails.setCountry(user.getCountry()); userDetails.setEmail(user.getEmail()); userDetails.setName(user.getName());
return userRepsoitory.save(userDetails); }
@DeleteMapping("users/{id}") public Boolean deleteUser(@PathVariable("id") Long id) {
User user = userRepsoitory.findById(id) .orElseThrow(() -> new ResourceNotFoundException ("User Not Found")); userRepsoitory.delete(user);
return true; }
@GetMapping("users/{id}") public User findById(@PathVariable("id") Long id) {
User user = userRepsoitory.findById(id) .orElseThrow(() -> new ResourceNotFoundException ("User Not Found")); return user; }}
Spring Boot Main Driver
@SpringBootApplicationpublic class SpringbootPaginationApplication {
public static void main(String[] args) { SpringApplication. run(SpringbootPaginationApplication.class, args); }}
Project 2: react-datatable-pagination
package.json
{ "name": "react-datatable-pagination", "version": "0.1.0", "private": true, "dependencies": { "@material-ui/core": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.57", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@fortawesome/fontawesome-free": "^5.15.3", "axios": "^0.27.2", "bootstrap": "^4.6.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "react-scripts": "5.0.1", "react-table": "^7.8.0", "web-vitals": "^1.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }}
/services/UserService.js
import http from "../http-common";
const get = (id) => { return http.get(`/users/${id}`);};
const create = (data) => { return http.post("/users", data);};
const update = (id, data) => { return http.put(`/users/${id}`, data);};
const remove = (id) => { return http.delete(`/users/${id}`);};
const getAll = (params) => { return http.get("/users", { params });};
const UserService = { getAll, get, create, update, remove};
export default UserService;
/components/User.js
import React, { useState, useEffect } from "react";import UserDataService from "../services/UserService";
const User = props => { const initialUserState = { id: null, country: "", email: "", name: "" }; const [currentUser, setCurrentUser] = useState(initialUserState); const [message, setMessage] = useState("");
const getUser = id => { UserDataService.get(id) .then(response => { setCurrentUser(response.data); console.log(response.data); }) .catch(e => { console.log(e); }); };
useEffect(() => { getUser(props.match.params.id); }, [props.match.params.id]);
const handleInputChange = event => { const { name, value } = event.target; setCurrentUser({ ...currentUser, [name]: value }); };
const updateUser = () => { UserDataService.update(currentUser.id, currentUser) .then(response => { console.log(response.data); setMessage("The User was updated successfully!"); }) .catch(e => { console.log(e); }); };
const deleteUser = () => { UserDataService.remove(currentUser.id) .then(response => { console.log(response.data); props.history.push("/Users"); }) .catch(e => { console.log(e); }); };
return ( <div> {currentUser ? ( <div className="edit-form"> <h4>User</h4> <form> <div className="form-group"> <label htmlFor="title">Name</label> <input type="text" className="form-control" id="name" name="name" value={currentUser.name} onChange={handleInputChange} /> </div> <div className="form-group"> <label htmlFor="email">Email</label> <input type="text" className="form-control" id="email" name="email" value={currentUser.email} onChange={handleInputChange} /> </div> <div className="form-group"> <label htmlFor="country">Country</label> <input type="text" className="form-control" id="country" name="country" value={currentUser.country} onChange={handleInputChange} /> </div>
</form>
<button type="button" onClick={deleteUser} class="btn btn-danger btn-sm">Delete</button> <button type="button" onClick={updateUser} class="btn btn-success btn-sm">Update</button>
<strong><p class="text-success">{message}</p></strong>
</div> ) : ( <div> <br /> <p>Click on a User...</p> </div> )} </div> );};
export default User;
/components/AddUser.js
import React, { useState } from "react";import UserDataService from "../services/UserService";
const AddUser = () => { const initialUserState = { id: null, country: "", email: "", name: "" }; const [User, setUser] = useState(initialUserState); const [submitted, setSubmitted] = useState(false);
const handleInputChange = event => { const { name, value } = event.target; setUser({ ...User, [name]: value }); };
const saveUser = () => { var data = { name: User.name, email: User.email, country: User.country };
UserDataService.create(data) .then(response => { setUser({ id: response.data.id, name: response.data.name, email: response.data.email, country: response.data.country }); setSubmitted(true); console.log(response.data); }) .catch(e => { console.log(e); }); };
const newUser = () => { setUser(initialUserState); setSubmitted(false); };
return ( <div className="edit-form"> {submitted ? ( <div> <strong><p class="text-success">Registration Successsful! </p></strong> <button className="btn btn-success" onClick={newUser}> Add </button> </div> ) : ( <div> <div className="form-group"> <label htmlFor="name">Name</label> <input type="name" className="form-control" id="name" required value={User.name} onChange={handleInputChange} name="name" /> </div>
<div className="form-group"> <label htmlFor="email">Email</label> <input type="text" className="form-control" id="email" required value={User.email} onChange={handleInputChange} name="email" /> </div>
<div className="form-group"> <label htmlFor="country">Country</label> <input type="text" className="form-control" id="country" required value={User.country} onChange={handleInputChange} name="country" /> </div>
<button onClick={saveUser} className="btn btn-success"> Submit </button> </div> )} </div> );};
export default AddUser;
/components/UsersList.js
import React, { useState, useEffect, useMemo, useRef } from "react";import Pagination from "@material-ui/lab/Pagination";import UserDataService from "../services/UserService";import { useTable } from "react-table";
const UsersList = (props) => { const [users, setUsers] = useState([]); const [searchCountry, setSearchCountry] = useState(""); const usersRef = useRef();
const [page, setPage] = useState(1); const [count, setCount] = useState(0); const [pageSize, setPageSize] = useState(4);
const pageSizes = [4, 8, 12];
usersRef.current = users;
const onChangeSearchCountry = (e) => { const searchCountry = e.target.value; setSearchCountry(searchCountry); };
const getRequestParams = (searchCountry, page, pageSize) => { let params = {};
if (searchCountry) { params["country"] = searchCountry; }
if (page) { params["page"] = page - 1; }
if (pageSize) { params["size"] = pageSize; }
return params; };
const retrieveUsers = () => { const params = getRequestParams(searchCountry, page, pageSize);
UserDataService.getAll(params) .then((response) => { const { users, totalPages } = response.data;
setUsers(users); setCount(totalPages);
console.log(response.data); }) .catch((e) => { console.log(e); }); };
useEffect(retrieveUsers, [page, pageSize]);
const findByCountry = () => { setPage(1); retrieveUsers(); };
const openUser = (rowIndex) => { const id = usersRef.current[rowIndex].id;
props.history.push("/users/" + id); };
const deleteUser = (rowIndex) => { const id = usersRef.current[rowIndex].id;
UserDataService.remove(id) .then((response) => { props.history.push("/users");
let newUsers = [...usersRef.current]; newUsers.splice(rowIndex, 1);
setUsers(newUsers); }) .catch((e) => { console.log(e); }); };
const handlePageChange = (event, value) => { setPage(value); };
const handlePageSizeChange = (event) => { setPageSize(event.target.value); setPage(1); };
const columns = useMemo( () => [ { Header: "Name", accessor: "name", }, { Header: "Email", accessor: "email", }, { Header: "Country", accessor: "country", }, { Header: "Actions", accessor: "actions", Cell: (props) => { const rowIdx = props.row.id; return ( <div> <span onClick={() => openUser(rowIdx)}> <button type="button" class="btn btn-warning btn-sm">Edit</button> </span> <span onClick={() => deleteUser(rowIdx)}> <button type="button" class="btn btn-danger btn-sm">Delete</button> </span> </div> ); }, }, ], [] );
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, } = useTable({ columns, data: users, });
return ( <div className="list row">
<div className="col-md-8"> <div className="input-group mb-3"> <input type="text" className="form-control" placeholder="Search by country" value={searchCountry} onChange={onChangeSearchCountry} /> <div className="input-group-append"> <button className="btn btn-outline-success" type="button" onClick={findByCountry} > Search </button> </div> </div> </div>
<div className="col-md-12 list"> <div className="mt-3"> {"Items per Page: "} <select onChange={handlePageSizeChange} value={pageSize}> {pageSizes.map((size) => ( <option key={size} value={size}> {size} </option> ))} </select>
<Pagination color="primary" className="my-3" count={count} page={page} siblingCount={1} boundaryCount={1} variant="outlined" onChange={handlePageChange} /> </div>
<table className="table table-striped table-bordered" {...getTableProps()} > <thead> {headerGroups.map((headerGroup) => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map((column) => ( <th {...column.getHeaderProps()}> {column.render("Header")} </th>
))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows.map((row, i) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map((cell) => { return ( <td {...cell.getCellProps()}> {cell.render("Cell")}</td> ); })} </tr> ); })} </tbody> </table>
<div className="mt-3"> {"Items per Page: "} <select onChange={handlePageSizeChange} value={pageSize}> {pageSizes.map((size) => ( <option key={size} value={size}> {size} </option> ))} </select>
<Pagination color="primary" className="my-3" count={count} page={page} siblingCount={1} boundaryCount={1} variant="outlined" onChange={handlePageChange} /> </div>
</div> </div> );};
export default UsersList;
http-common.js
import axios from "axios";
export default axios.create({ baseURL: "http://localhost:8080/api/v1", headers: { "Content-type": "application/json" }});
App.js
import React from "react";import { Switch, Route, Link } from "react-router-dom";import "bootstrap/dist/css/bootstrap.min.css";import "./App.css";import "@fortawesome/fontawesome-free/css/all.css";import "@fortawesome/fontawesome-free/js/all.js";
import AddUser from "./components/AddUser";import User from "./components/User";import UsersList from "./components/UsersList";
function App() { return ( <div> <nav className="navbar navbar-expand navbar-light bg-light"> <a href="/users" className="navbar-brand"> KnowledgeFactory </a> <div className="navbar-nav mr-auto"> <li className="nav-item"> <Link to={"/users"} className="nav-link"> Users </Link> </li> <li className="nav-item"> <Link to={"/add"} className="nav-link"> Add User </Link> </li> </div> </nav>
<div className="container mt-3"> <Switch> <Route exact path={["/", "/users"]} component={UsersList} /> <Route exact path="/add" component={AddUser} /> <Route path="/users/:id" component={User} /> </Switch> </div> </div> );}
export default App;
App.css
.list .action { cursor: pointer;}
.submit-form { max-width: 4ex; margin: auto;}
.edit-form { max-width: 400px; margin: auto;}
index.js
import React from "react";import ReactDOM from "react-dom";import { BrowserRouter } from "react-router-dom";
import App from "./App";import reportWebVitals from './reportWebVitals';
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("root"));
reportWebVitals();
index.css
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}
code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;}
Download the complete source code - click here
Local Setup and Run the application
Step1: Download or clone the source code from GitHub to a local machine - Click here
Backend
Step 2: mvn clean install
Step 3: Run the Spring Boot application - mvn spring-boot:run
Frontend
Step 4: npm install
Step 5: npm start