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:



Frontend 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

@ControllerAdvice
public 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

@SpringBootApplication
public 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>&nbsp;
<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>
&nbsp;
<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

From the browser call the endpoint http://localhost:3000/

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Spring boot video streaming example-HTML5

Spring Boot + Mockito simple application with 100% code coverage

Spring Boot + OpenCSV Export Data to CSV Example

Custom Exception Handling in Quarkus REST API

Registration and Login with Spring Boot + Spring Security + Thymeleaf

DataTable-Pagination example with Spring boot, jQuery and ajax

Spring Webflux File Download - REST API Example

Node JS mini projects with source code - free download

ReactJS, Spring Boot JWT Authentication Example