DEV Community

loading...

Build A Todo App with React, MongoDB, ExpressJS, and NodeJS Part 2 (Frontend)

htnguy profile image Hieu Nguyen Originally published at devsurvival.com Updated on ・6 min read

Welcome back. Congratulation on completing part 1 of the tutorial on how to create a todo app with React and NodeJS.

In part 2, we will create the react frontend and connect it to our API backend to GET, POST, UPDATE, and DELETE our todos.

Additional Packages

Before we can start coding, we have to install some additional packages to make this work.

  1. Axios - allows us to send http request from out react frontend to our todo API run npm install axios in the todo-frontend directory
  2. Cors - allows cross domain http request. In other words, without enabling cors on the backend, even Axios will not able to send our request to the API. run npm install cors in the todo-backend directory, and then add the snippet below to the top of your index.js file in the root of todo-backend directory
const cors = require("cors")
app.use(cors())

Almost There :)

Since the frontend for this application is pretty straight forward, we are going to make changes to two files: App.js and the APIHelper.js (we will have to create)

Let's create the APIHelper.js file in the src directory of the todo-frontend .

touch APIHelper.js

Copy the following code into the APIHelper.js file

import axios from "axios"

const API_URL = "http://localhost:3000/todos/"

async function createTodo(task) {
  const { data: newTodo } = await axios.post(API_URL, {
    task,
  })
  return newTodo
}

async function deleteTodo(id) {
  const message = await axios.delete(`${API_URL}${id}`)
  return message
}

async function updateTodo(id, payload) {
  const { data: newTodo } = await axios.put(`${API_URL}${id}`, payload)
  return newTodo
}

async function getAllTodos() {
  const { data: todos } = await axios.get(API_URL)
  return todos
}

export default { createTodo, deleteTodo, updateTodo, getAllTodos }

Let Me Explain

We have four functions that mimic our API createTodo, deleteTodo, updateTodo, getAllTodos .

createTodo(task) - accepts a task and sends a post via axios.post to our API_URL and returns the newTodo. Note: axios stores the response of our requests in a field called data,

deleteTodo(id) - accepts an id and sends a delete request to our API.

updateTodo - accepts an id and a payload object contain fields that we want to update => payload= {completed: true} .It sends a PUT request to update the todo.

getAllTodos - fetching all the todos from our API via axios.get

And we make all these functions accessible in other files using an export function export default { createTodo, deleteTodo, updateTodo, getAllTodos };

App.js

Copy the following code into your App.js file

import React, { useState, useEffect } from "react"
import "./App.css"
import APIHelper from "./APIHelper.js"

function App() {
  const [todos, setTodos] = useState([])
  const [todo, setTodo] = useState("")

  useEffect(() => {
    const fetchTodoAndSetTodos = async () => {
      const todos = await APIHelper.getAllTodos()
      setTodos(todos)
    }
    fetchTodoAndSetTodos()
  }, [])

  const createTodo = async e => {
    e.preventDefault()
    if (!todo) {
      alert("please enter something")
      return
    }
    if (todos.some(({ task }) => task === todo)) {
      alert(`Task: ${todo} already exists`)
      return
    }
    const newTodo = await APIHelper.createTodo(todo)
    setTodos([...todos, newTodo])
  }

  const deleteTodo = async (e, id) => {
    try {
      e.stopPropagation()
      await APIHelper.deleteTodo(id)
      setTodos(todos.filter(({ _id: i }) => id !== i))
    } catch (err) {}
  }

  const updateTodo = async (e, id) => {
    e.stopPropagation()
    const payload = {
      completed: !todos.find(todo => todo._id === id).completed,
    }
    const updatedTodo = await APIHelper.updateTodo(id, payload)
    setTodos(todos.map(todo => (todo._id === id ? updatedTodo : todo)))
  }

  return (
    <div className="App">
      <div>
        <input
          id="todo-input"
          type="text"
          value={todo}
          onChange={({ target }) => setTodo(target.value)}
        />
        <button type="button" onClick={createTodo}>
          Add
        </button>
      </div>

      <ul>
        {todos.map(({ _id, task, completed }, i) => (
          <li
            key={i}
            onClick={e => updateTodo(e, _id)}
            className={completed ? "completed" : ""}
          >
            {task} <span onClick={e => deleteTodo(e, _id)}>X</span>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default App

Let Me Explain

We start by creating two states: todo and todos. States are like information about your components. todo will store the user input when creating a new todo and todos will store all of our todos.

Lets see what the component looks like on paper.

return (
  <div className="App">
    <div>
      <input
        id="todo-input"
        type="text"
        value={todo}
        onChange={({ target }) => setTodo(target.value)}
      />
      <button type="button" onClick={createTodo}>
        Add
      </button>
    </div>

    <ul>
      {todos.map(({ _id, task, completed }, i) => (
        <li
          key={i}
          onClick={e => updateTodo(e, _id)}
          className={completed ? "completed" : ""}
        >
          {task} <span onClick={e => deleteTodo(e, _id)}>X</span>
        </li>
      ))}
    </ul>
  </div>
)

To keep things simple we have a text input, a button for submitting the input, and a list.

The text input has an onChange event handler for handling user inputs. When the user clicks the Add button, the onClick event handler is triggered- createTodo() is invoked.

Creating Todo

lets look at what the createTodo function does

const createTodo = async e => {
  e.preventDefault()
  if (!todo) {
    // check if the todo is empty
    alert("please enter something")
    return
  }
  if (todos.some(({ task }) => task === todo)) {
    // check if the todo already exists
    alert(`Task: ${todo} already exists`)
    return
  }
  const newTodo = await APIHelper.createTodo(todo) // create the todo
  setTodos([...todos, newTodo]) // adding the newTodo to the list
}

Overall, it validates the input, create the todo using the APIHelper.js we created, and then add it to the list of todos

Displaying the Todos

<ul>
  {todos.map(({ _id, task, completed }, i) => (
    <li
      key={i}
      onClick={e => updateTodo(e, _id)}
      className={completed ? "completed" : ""}
    >
      {task} <span onClick={e => deleteTodo(e, _id)}>X</span>
    </li>
  ))}
</ul>

We are mapping over the list of todos and creating a new list item with li

How do we load the todos when the page loads? React offers a useful function call useEffect which is called after the component is rendered

useEffect(() => {
  const fetchTodoAndSetTodos = async () => {
    const todos = await APIHelper.getAllTodos()
    setTodos(todos)
  }
  fetchTodoAndSetTodos()
}, [])

we create an async function called fetchTodoAndSetTodos which call the APIHelper's getAllTodos function to fetch all the todos. It then sets the todos state of the component to include these todos.

Marking Todo As Completed

;(
  <li
    key={i}
    onClick={e => updateTodo(e, _id)}
    className={completed ? "completed" : ""}
  >
    {task} <span onClick={e => deleteTodo(e, _id)}>X</span>
  </li>
)``

When the task is completed we add the class completed. you can declare this css class in a separate file. create-react-app provides an App.css file for this purpose.

.completed {
  text-decoration: line-through;
  color: gray;
}

Notice each todo item (<li onClick={updateTodo}>{task}</li>) has an onClick event handler. When we click an li we trigger the updateTodo function.

const updateTodo = async (e, id) => {
  e.stopPropagation()
  const payload = {
    completed: !todos.find(todo => todo._id === id).completed,
  }
  const updatedTodo = await APIHelper.updateTodo(id, payload)
  setTodos(todos.map(todo => (todo._id === id ? updatedTodo : todo)))
}

e is the event object on which we invoked e.stopPropagation() to prevent the click event from propagating to the parent element. Next, we find the todo in the list of todos and flip its completed status(completed = true => !completed == false) . We add this new completed status to the payload object. we then call APIHelper.updateTodo and pass in the id and payload of the todo.

The next bit of code is a little confusing. we call todos.map which maps over the array and return a new array. With each iteration we are check if the id matches. If it matches, then we return the updatedTodo which is effectively updating the todo. Otherwise, we return the original todo and leave it unchanged.

Deleting a Todo

<li
  key={i}
  onClick={e => updateTodo(e, _id)}
  className={completed ? "completed" : ""}
>
  {task} <span onClick={e => deleteTodo(e, _id)}>X</span>
</li>

Notice how we have a <span onClick={DeleteTodo(e, _id)}>X</span> next to the task. When this span is clicked, it triggers the deleteTodo function that will delete the todo.

Here is the function for deleting the todo.

const deleteTodo = async (e, id) => {
  try {
    e.stopPropagation()
    await APIHelper.deleteTodo(id)
    setTodos(todos.filter(({ _id: i }) => id !== i))
  } catch (err) {}
}

we call APIHelper.deleteTodo and pass in the id of the todo we want to delete. If you refresh the page, the todo will be deleted. What if you were lazy and did not feel like refreshing the page or you didn't know better? Well, we have to remove it manually from the todos state. We remove it by calling todos.filter which will filter out the todo with the id we just deleted.

Show Time

Here is a quick demo:

This tutorial's source code can be found on github

originally posted at https://www.devsurvival.com/todo-app-react-frontend/

Discussion (0)

pic
Editor guide