Today, we are going to create a todo app with React and Node JS. A to-do app, short for “to-do list application,” is a software product that assists people or teams in better organising activities and managing their time. It normally lets users to create, organise, and prioritise tasks in a list style, set reminders and due dates, and mark completed tasks.
In this article, we will see how we can create this this todo application with React, Node JS and MySQL using Sequelize. We will use React for the frontend and Node JS for the API creation.
Sequelize is a Node.js Object-Relational Mapping (ORM) framework that allows you to connect with relational databases using JavaScript syntax. It works with a variety of database systems, including MySQL, PostgreSQL, SQLite, and MSSQL.
When used in conjunction with MySQL, Sequelize offers a number of useful capabilities for managing database connections, accessing data, and executing migrations. It abstracts away the actual SQL queries and enables developers to deal with database tables as JavaScript objects, utilising a robust set of API methods.
Some of the key features of Sequelize with MySQL include:
To utilise serialisation in MySQL, you must first identify the essential areas of your application where concurrent access to the same data is possible, and then apply proper locking methods to avoid conflicts. Excessive locking can cause conflict and lower concurrency, therefore it’s critical to achieve a balance between performance and data consistency.
Now let us begin …
First, we have to create a new React app
npx create-react-app folderName
Next, we need to add some dependencies — react-router-dom, sweetealert2. This will help us to show the results of performed actions.
yarn add react-router-dom sweetalert2
From inside the src folder, we will create a new folder called assets -> images. We will also create another folder called components — that’s where we will put our API component. We will also create a folder called pages still inside the src folder.
We will modify our App.js file as follows:
import React, { Component } from 'react';
import {BrowserRouter, Router, HashRouter, Switch, Route, Navigate} from 'react-router-dom';
import Todo from './pages/Todos';
class App extends Component {
render(){
return(
<HashRouter>
<Switch>
<Route exact path="/" component={Todo} />
</Switch>
</HashRouter>
)
}
}
export default App;
Next, we will create a file called Todos.js in our pages folder
import React, { Component } from 'react';
import logo from '../assets/images/logo.png';
import { baseUrl } from '../components/BaseUrl';
import Swal from "sweetalert2";
class Todos extends Component {
render(){
return(
<div>
</div>
)
}
}
export default Todos
Next, we are going to create our Node.js project. To do that you have to first create a folder in your computer, go to the directory in your command prompt or terminal and run the following command:
npm init --yes
Follow the prompts on the screen after which a `package.json`
file will automatically be created.
Now, we want to add some dependencies to our project. Let’s add the following dependencies:
yarn add body-parser express cars mysql2 sequelize
Your `package.json` file should now look like this:
Next, let’s create a file named `server.js
`. In this file we are going to create 3 endpoints as follows:
Add the following code to your `server.js` file
const express = require('express');
const bodyParser = require('body-parser');
const { Sequelize, DataTypes } = require('sequelize');
const cors = require('cors');
const sequelize = new Sequelize('database_name', 'user_name', 'password', {
host: 'localhost',
dialect: 'mysql'
});
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());
const Task = sequelize.define('Task', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.STRING,
allowNull: true
},
completed: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
});
app.get('/todoApp/tasks/', async (req, res) => {
const tasks = await Task.findAll();
res.json(tasks);
});
app.post('/todoApp/tasks/', async (req, res) => {
const task = {
title: req.body.title,
completed: req.body.completed ? req.body.completed : false
};
Task.create(task)
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message:
err.message || "Some error occurred while creating the Task."
});
});
// const task = await Task.create({
// title: req.body.title,
// // description: req.body.description,
// completed: false
// });
// res.json(task);
});
app.put('/todoApp/tasks/:id', async (req, res) => {
const task = await Task.findByPk(req.params.id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
task.title = req.body.title;
task.description = req.body.description;
task.completed = true;
await task.save();
res.json(task);
});
app.delete('/todoApp/tasks/:id', async (req, res) => {
const task = await Task.findByPk(req.params.id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
await task.destroy();
res.json({ message: 'Task deleted' });
});
sequelize.sync().then(() => {
app.listen(3000, () => {
console.log('Server started on port 3000');
});
});
Replace this line in the above code with your database credentials:
const sequelize = new Sequelize('database_name', 'user_name', 'password'
You can even pause here and test your application with Postman and evaluate your response.
First let us create a file called `BaseUrl.js` and add the following code:
const baseUrl = "https://localhost:3000/"
export { baseUrl }
Now, let us go back to our `Todos.js` file and add the following code:
import React, { Component } from 'react';
import logo from '../assets/images/logo.png';
import { baseUrl } from '../components/BaseUrl';
import Swal from "sweetalert2";
class Todos extends Component {
constructor(props){
super(props);
this.state = {
tasks: [],
item: "",
title: "",
updateId: "",
updateTitle: "",
isAddingItem: false,
isDisabled: false,
isFetchingTasks: false,
isUpdatingStatus: false,
isRemovingItem: false,
postsPerPage: 10,
currentPage: 1,
}
}
componentDidMount(){
this.getTodoList()
}
getTask = (id) => {
let params = id.split(",")
this.setState({updateId: params[0], updateTitle: params[1]})
}
//=========================================
//START OF UPDATE TASK
//=========================================
updateTask = async (id) => {
console.warn(id);
// let params = id.split(",")
this.setState({isUpdatingStatus: true})
let req = {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: this.state.title ? this.state.title : this.state.updateTitle,
description: null,
completed: true,
}),
};
await fetch(`${baseUrl}todoApp/tasks/${this.state.updateId}`, req)
.then((response) => response.json())
.then((responseJson) => {
console.warn(responseJson);
if(responseJson && responseJson.id){
this.setState({isUpdatingStatus: false, isDisabled: false})
Swal.fire({
title: "Success",
text: 'Status updated successfully!',
icon: "success",
confirmButtonText: "OK",
}).then(() => {
window.location.reload()
})
}else{
this.setState({isUpdatingStatus: false, isDisabled: false})
Swal.fire({
title: "Error!",
text: 'An error occurred. Please try again later.',
icon: "error",
confirmButtonText: "OK",
});
}
})
.catch((error) => {
this.setState({isUpdatingStatus: false, isDisabled: false})
Swal.fire({
title: "Error!",
text: error.message,
icon: "error",
confirmButtonText: "OK",
});
})
}
//=========================================
//END OF UPDATE TASK
//=========================================
//=========================================
//START OF DELETE TASK
//=========================================
deleteTask = (id) => {
this.setState({isRemovingItem: true})
const url = `${baseUrl}todoApp/tasks/${id}`;
fetch(url, {
method: 'DELETE',
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
})
.then(res => res.json())
.then(res => {
// console.warn(res);
if(res.message === "Task deleted"){
this.setState({isRemovingItem: false});
Swal.fire({
title: "Success",
text: "Task removed successfully",
icon: "success",
confirmButtonText: "OK",
}).then(() => {
window.location.reload()
})
}else{
Swal.fire({
title: "Error",
text: "An error occurred, please try again",
icon: "error",
confirmButtonText: "OK",
})
this.setState({isRemovingItem: false});
}
})
.catch(error => {
this.setState({isRemovingItem: false});
alert(error);
});
}
//=========================================
//END OF DELETE TASK
//=========================================
//=========================================
//START OF GET ALL TASK
//=========================================
getTodoList = async () => {
this.setState({ isFetchingTasks: true})
let obj = {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
};
await fetch(`${baseUrl}todoApp/tasks`, obj)
.then((response) => response.json())
.then((responseJson) => {
// console.warn(responseJson);
if (responseJson) {
this.setState({ tasks: responseJson, isFetchingTasks: false });
}else{
this.setState({ isFetchingTasks: false })
Swal.fire({
title: "Error!",
text: "Could not retrieve todo list. Please try again later",
icon: "error",
confirmButtonText: "OK",
})
}
})
.catch((error) => {
this.setState({ isFetchingTasks: false })
Swal.fire({
title: "Error!",
text: error.message,
icon: "error",
confirmButtonText: "OK",
});
});
}
//=========================================
//END OF GET ALL TASK
//=========================================
showTasks = () => {
const { postsPerPage, currentPage, isRemovingItem, tasks } = this.state;
const indexOfLastPost = currentPage * postsPerPage;
const indexOfFirstPost = parseInt(indexOfLastPost) - parseInt(postsPerPage);
const currentPosts = tasks.slice(indexOfFirstPost, indexOfLastPost);
try {
return currentPosts.map((item, index) => {
return (
<tr>
<td className="text-xs font-weight-bold">{index +1}</td>
<td className="text-xs text-capitalize font-weight-bold">{item.title}</td>
<td className={item.completed.toString() === "true" ? 'badge bg-success mt-3' : "badge bg-warning mt-3" }>{item.completed.toString()}</td>
<td><button className="btn btn-success" data-bs-toggle="modal" data-bs-target="#update" id={item.id} onClick={() => this.getTask(`${item.id}, ${item.title}, ${item.completed.toString()}`)}>Update</button>
<div className="modal fade" id="update" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header d-flex align-items-center justify-content-between bg-danger">
<h5 className="modal-title text-light">Update Task</h5>
<button type="button" className="btn btn-link m-0 p-0 text-dark fs-4" data-bs-dismiss="modal" aria-label="Close"><span class="iconify" data-icon="carbon:close"></span></button>
</div>
<div className="modal-body">
<div className="row">
<div clasNames="d-flex text-center">
<div className="d-flex flex-column">
<div className="mb-3">
<input type="text" className="form-control" id="email" aria-describedby="email"
placeholder="Title"
defaultValue={this.state.updateTitle}
onChange={(e) =>
this.setState({ title: e.target.value })
}
/>
</div>
</div>
<div className="col-sm-12 col-lg-12 col-md-12 mb-3">
<label className="text-dark" htmlFor="role">Status</label>
<select
className="form-control shadow-none"
aria-label="Floating label select example"
onChange={this.handleApprovalChange}
id="role"
>
<option selected disabled>--Select Task Status --</option>
<option value="true">TRUE</option>
<option value="false">FALSE</option>
</select>
</div>
<div className="text-center" id={item.id}><button disabled={this.state.isDisabled} onClick={(e) => this.updateTask(item.id)} type="button" class="btn btn-success font-weight-bold px-5 mb-5 w-100">
{this.state.isUpdatingStatus ? (
'updating ...'
) : (
"Update Task"
)}
</button></div>
<div class="modal-footer">
<button type="button" data-bs-dismiss="modal" class="btn btn-primary">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</td>
<td><button className="btn btn-danger" id={item.id} onClick={() => this.deleteTask(`${item.id}`)}>Delete</button></td>
</tr>
);
});
} catch (e) {
// Swal.fire({
// title: "Error",
// text: e.message,
// type: "error",
// })
}
}
//=========================================
//START OF PAGINATION
//=========================================
showPagination = () => {
const { postsPerPage, tasks } = this.state;
const pageNumbers = [];
const totalPosts = tasks.length;
for(let i = 1; i<= Math.ceil(totalPosts/postsPerPage); i++){
pageNumbers.push(i)
}
const paginate = (pageNumbers) => {
this.setState({currentPage: pageNumbers})
}
return(
<nav>
<ul className="pagination mt-4" style={{float: 'right', position: 'relative', right: 54}}>
{pageNumbers.map(number => (
<li key={number} className={this.state.currentPage === number ? 'page-item active' : 'page-item'}>
<button onClick={()=> paginate(number)} className="page-link">
{ number }
</button>
</li>
))}
</ul>
</nav>
)
}
//=========================================
//END OF PAGINATION
//=========================================
//=========================================
// START OF CREATE TODO TASK
//=========================================
createTodo = async () => {
const { item } = this.state;
this.setState({isAddingItem: true, isDisabled: true})
let req = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `${item}`,
description: "hello",
completed: false,
}),
};
await fetch(`${baseUrl}todoApp/tasks`, req)
.then((response) => response.json())
.then((responseJson) => {
// console.warn(responseJson);
if(responseJson){
this.setState({isAddingItem: false, isDisabled: false})
Swal.fire({
title: "Success",
text: 'Task added successfully!',
icon: "success",
confirmButtonText: "OK",
}).then(() => {
window.location.reload()
})
}else{
this.setState({isAddingItem: false, isDisabled: false})
Swal.fire({
title: "Error!",
text: 'An error occurred. Please try again later.',
icon: "error",
confirmButtonText: "OK",
});
}
})
.catch((error) => {
this.setState({isAddingItem: false, isDisabled: false})
Swal.fire({
title: "Error!",
text: error.message,
icon: "error",
confirmButtonText: "OK",
});
})
}
//=========================================
//END OF CREATE TODO TASK
//=========================================
render(){
const { isAddingItem, isUpdatingStatus, isRemovingItem, isFetchingTasks } = this.state;
return(
<div className="container">
<div className="text-center">
<img src={logo} className="img-fluid profile-image-pic img-thumbnail my-3"
width="200px" alt="logo" />
</div>
<div className="todo-container">
<div className="todo-app">
<form className="add-todo-form">
<input className="add-todo-input" type="text" placeholder="Add item" onChange={(e) =>
this.setState({ item: e.target.value })
} />
<button type="button" onClick={(e) => this.createTodo()} className="add-todo-btn">
{isAddingItem ? (
'adding item ...'
) : (
"Add"
)}
</button>
</form>
<div className="container-fluid py-4">
<div className="table-responsive p-0 pb-2">
{isRemovingItem && <p className="text-center text-danger">Removing item ...</p>}
{isUpdatingStatus && <p className="text-center text-success">Updating Status ...</p>}
<table className="table align-items-center justify-content-center mb-0">
<thead>
<tr>
<th className="text-uppercase text-secondary text-sm font-weight-bolder opacity-7 ps-2">S/N</th>
<th className="text-uppercase text-secondary text-sm font-weight-bolder opacity-7 ps-2">Task</th>
<th className="text-uppercase text-secondary text-sm font-weight-bolder opacity-7 ps-2">Completed</th>
<th className="text-uppercase text-secondary text-sm font-weight-bolder opacity-7 ps-2">Update</th>
<th className="text-uppercase text-secondary text-sm font-weight-bolder opacity-7 ps-2">Delete</th>
</tr>
</thead>
{isFetchingTasks ? <div style={{ position: 'relative', top: 10, left: 250}} class="spinner-border text-success" role="status">
<span className="sr-only">Loading...</span>
</div> :
<tbody>
{this.showTasks()}
</tbody>
}
</table>
</div>
<div style={{float: 'right'}}>
{this.showPagination()}
</div>
</div>
</div>
</div>
{/* Update Modal */}
</div>
)
}
}
export default Todos;
So a bit of explanation here. We can add items, delete items, update the task status and fetch all items. We are also adding pagination to our application, which means we are displaying the first 10 items and rest later. We are also using SweetAlert to show event notifications.
Here is how our application looks like:
Here are the GitHub Repository Links to the project
Node Backend: https://github.com/Luckae/todo-backend
React Frontend: https://github.com/Luckae/todo-react
If this project helped you, don’t forget to follow us and give us a star.
Get Started With React Native Flexbox
Introduction The Observer Pattern is a design pattern used to manage and notify multiple objects…
Memory management is like housekeeping for your program—it ensures that your application runs smoothly without…
JavaScript has been a developer’s best friend for years, powering everything from simple websites to…
In the digital age, web development plays a crucial role in shaping how individuals interact…
Introduction Handling large amounts of data efficiently can be a challenge for developers, especially when…