javascript

Create a Todo App With React, Node JS And MySQL Using Sequelize And Pagination

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.

What is Sequelize in MySQL?

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:

  • Support for database migrations: Sequelize provides a way to easily create, update, and roll back database schema changes, making it easy to manage database schema changes in a controlled and repeatable way.
  • Object-Relational Mapping: Sequelize maps database tables to JavaScript objects, allowing developers to interact with the database using JavaScript syntax and avoid writing raw SQL queries.
  • Query building: Sequelize provides a powerful query builder that allows developers to create complex SQL queries using a simple and intuitive syntax.
  • Connection pooling: Sequelize includes a built-in connection pool, which can help improve performance by reusing database connections rather than creating a new connection for every request.
  • Transactions: Sequelize supports database transactions, which provide a way to group together a set of database operations that should be executed atomically.

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 …

Building the Frontend With React

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

Create The Node.JS project

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:

Congrats! You’ve made some cool progress. But we’re not done yet.

Next, let’s create a file named `server.js`. In this file we are going to create 3 endpoints as follows:

  1. Create new task
  2. Update task
  3. Delete task
  4. Get all created task

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.

Call the Endpoints in the React Application

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:

Create Todo app with React and Node JS using Sequelize.

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

Author

Recent Posts

Apple is developing a doorbell camera equipped with Face ID technology.

Apple is reportedly developing a new smart doorbell camera with Face ID technology to unlock…

3 hours ago

Google Launches Its Own ‘Reasoning’ AI Model to Compete with OpenAI

This month has been packed for Google as it ramps up efforts to outshine OpenAI…

2 days ago

You can now use your phone line to call ChatGPT when cellular data is unavailable.

OpenAI has been rolling out a series of exciting updates and features for ChatGPT, and…

4 days ago

Phishers use fake Google Calendar invites to target victims

A financially motivated phishing campaign has targeted around 300 organizations, with over 4,000 spoofed emails…

4 days ago

Hackers Exploiting Microsoft Teams to Remotely Access Users’ Systems

Hackers are exploiting Microsoft Teams to deceive users into installing remote access tools, granting attackers…

5 days ago

Ethical Hacking Essentials

Data plays an essential role in our lives.  We each consume and produce huge amounts…

7 days ago