Build A Todo App with React, MongoDB, ExpressJS, and NodeJS Part 2 (Frontend)
Hieu Nguyen · March 23, 2020 · 8 min read
webdevnodejsreactjsexpressjsmongodb
0
0 leave some love!
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.
- Axios - allows us to send http request from out react frontend to our todo API run
npm install axios
in thetodo-frontend
directory - 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 thetodo-backend
directory, and then add the snippet below to the top of yourindex.js
file in the root oftodo-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.
Let’s 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 feel like refreshing 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