28
MERN stack TODO application [Frontend]
We'll be creating the frontend application for our backend application.
In part-1, we
npm
and installed necessary dependenciesMongoDB
databasenode.js
and express.js
schema
to define a TODO
create
, read
, update
and delete
todocreate-react-app
The folder structure will look something like this
.
└── mern-todo
├── server
└── client
We'll initialize the
create-react-app
in the client
folder. Run the following command from the terminal but make sure you are in the client
folder.npx create-react-app .
The
.
in the above command refers to the current folder
. This will install our react app in the current folder instead of installing the app in a different folder.Within the client folder install the following dependencies
npm i node-sass axios react-router-dom
node-sass
: allows using sass
instead of css
axios
: to make api calls to the backend
react-router-dom
: for routing between pagesclient
's folders package.json
should look something like this.{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.21.1",
"node-sass": "^6.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"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"
]
}
}
logo.svg
App.js
App.js
<header className="App-header">
<img src="{logo}" className="App-logo" alt="logo" />
<p>Edit <code>src/App.js</code> and save to reload.</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
and put the following placeholder. We will put the actual code later.
<header>Hello, World!</header>
Delete the index.css
file and remove the corresponding import from index.js
Rename the App.css
file to App.scss
and change the corresponding import at App.js
import "./App.scss" //updated
Run npm start
. Open http://localhost:3000
and it should display Hello, World!
Copy and paste the styles from here and paste it in the App.scss
file.
Now, we are good to start creating the frontend application.
.
├── node_modules
├── public
├── src <---------- we are here
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
Create a
components
folder inside the src
folder and add the following filescreateTodo.jsx
showTodoList.jsx
updateTodo.jsx
After adding these files, the folder structure will look something like this
.
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx
│ │ ├── showTodoList.jsx
│ │ └── updateTodo.jsx
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
.
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx
│ │ ├── showTodoList.jsx <-- we are here
│ │ └── updateTodo.jsx
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
First, we will create the
ShowTodoList
component, to read all the documents that we created in the previous part while testing the backend application.useState
and useEffect
hooks from react
axios
from axios
In
ShowTodoList
function component will have a state todo
, we will fetch the documents from the database and store it in the state todo
.We will use
axios
to send a GET
request to the backend to fetch the document. Upon receiving the data we will store the data in todo
using setTodo
and log the data. If we receive an error we'll log that too.We will make the get request from the
useEffect
hook, since we want the data to load when the page loads.We will use the
TodoCard
component to display the contents of the todo
. We will use map
to iterate over todo
and pass the contents to TodoCard
which will display the contents of each todo document.The contents of the
showTodoList.jsx
file should look something like this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/components/showTodoList.jsx | |
import { useState, useEffect } from "react"; | |
import axios from "axios"; | |
function TodoCard({ data }) { | |
const { _id, title, description } = data; | |
return ( | |
<li key={_id}> | |
<div className="title-description"> | |
<h3>{title}</h3> | |
<p>{description}</p> | |
</div> | |
<div className="button-container"> | |
<button className="button">edit</button> | |
<button className="button">delete</button> | |
</div> | |
</li> | |
); | |
} | |
export function ShowTodoList() { | |
const [todo, setTodo] = useState([]); | |
useEffect(() => { | |
axios | |
.get("http://localhost:8000/api/todo") | |
.then((res) => { | |
console.log(res.data); | |
setTodo(res.data); | |
}) | |
.catch((err) => { | |
console.log(err); | |
}); | |
}, []); | |
return ( | |
<section className="container"> | |
<section className="contents"> | |
<h1>TODO</h1> | |
<ul className="list-container"> | |
{todo.map((data) => ( | |
<TodoCard data={data} /> | |
))} | |
</ul> | |
</section> | |
</section> | |
); | |
} |
We will import
ShowTodoList
component in the App.js
fileThe contents of the
App.js
file should look something like this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/App.js | |
import { ShowTodoList } from "./components/showTodoList"; | |
import "./App.scss"; | |
function App() { | |
return ( | |
<div className="app-contents"> | |
<ShowTodoList /> | |
</div> | |
); | |
} | |
export default App; |
Now, start the
server
that we built in part-1
npm run dev
Now, start the
client
side applicationnpm start
Open
http://localhost:3000
in your browser and it should display all the todo documents that was fetched from the database..
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx <-- we are here
│ │ ├── showTodoList.jsx
│ │ └── updateTodo.jsx
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
To create a new document we will send a
POST
request to our server
using axios.useState
hook react
Link
from react-router-dom
handleChange
that will get the input datahandleSubmit
that will send the POST
request to the server
data
using useState
hook with the following json
{
"title": "",
"description": ""
}
In
handleChange
method we will update the data
when the input changes. We will call the setData()
and declare a arrow function inside that will copy the contents of the previous data if any exists. In this e.target.name
will be the name of the input element that will have either title
or description
.In
handleSubmit
method,e.preventDefault()
to prevent the page from reloading when the submit button is clicked.POST
request to the server with the data. If the data has been sent successfully to the server then reset the state data
After adding the above change the code will look something like this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/components/createTodo.jsx | |
import { useState } from "react"; | |
import { Link } from "react-router-dom"; | |
import axios from "axios"; | |
export function CreateTodo() { | |
const [data, setData] = useState({ title: "", description: "" }); | |
function handleChange(e) { | |
setData((data) => ({ ...data, [e.target.name]: e.target.value })); | |
} | |
function handleSubmit(e) { | |
e.preventDefault(); | |
const todo = { | |
title: data.title, | |
description: data.description, | |
}; | |
console.log({ todo }); | |
axios | |
.post("http://localhost:8000/api/todo", data) | |
.then((res) => { | |
setData({ title: "", description: "" }); | |
console.log(res.data.message); | |
}) | |
.catch((err) => { | |
console.log("Error couldn't create TODO"); | |
console.log(err.message); | |
}); | |
} | |
return ( | |
<section className="container"> | |
<Link to="/" className="button-back"> | |
<button type="button" className="button"> | |
back | |
</button> | |
</Link> | |
<section className="contents"> | |
<form | |
onSubmit={handleSubmit} | |
className="form-container" | |
noValidate | |
> | |
<label className="label" htmlFor="title"> | |
Title | |
</label> | |
<input | |
type="text" | |
name="title" | |
value={data.title} | |
onChange={handleChange} | |
className="input" | |
/> | |
<label className="label" htmlFor="description"> | |
Description | |
</label> | |
<input | |
type="text" | |
name="description" | |
value={data.description} | |
onChange={handleChange} | |
className="input" | |
/> | |
<button type="submit" className="button"> | |
create todo | |
</button> | |
</form> | |
</section> | |
</section> | |
); | |
} |
.
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx
│ │ ├── showTodoList.jsx
│ │ └── updateTodo.jsx
│ ├── App.js <-------------- we are here
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
Before we can use the
CreateTodo
component we need to update App.js
file.BrowserRouter
and Route
from react-router-dom
CreateTodo
component from components/createTodo
Route
for home page /
and pass the ShowTodoList
componentRoute
for creating a new todo /create-todo
Route
s inside of the BrowserRouter
After making the changes the
App.js
file should look something like this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/App.js | |
import { BrowserRouter, Route } from "react-router-dom"; | |
import { ShowTodoList } from "./components/showTodoList"; | |
import { CreateTodo } from "./components/createTodo"; | |
import "./App.scss"; | |
function App() { | |
return ( | |
<div className="app-contents"> | |
<BrowserRouter> | |
<Route exact path="/" component={ShowTodoList} /> | |
<Route path="/create-todo" component={CreateTodo} /> | |
</BrowserRouter> | |
</div> | |
); | |
} | |
export default App; |
Since we have not added the button to navigate to
http://localhost:3000/create-todo
you can type this in your browser to check the CreateTodo
component..
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx
│ │ ├── showTodoList.jsx <-- we are here
│ │ └── updateTodo.jsx
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
Link
from react-router-dom
button
inside of Link
tagAfter making the changes, the
ShowTodoComponent
will look something like this.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/components/showTodoList.jsx | |
import { useState, useEffect } from "react"; | |
import axios from "axios"; | |
import { Link } from "react-router-dom"; // added | |
function TodoCard({ data }) { | |
const { _id, title, description } = data; | |
return ( | |
<li key={_id}> | |
<div className="title-description"> | |
<h3>{title}</h3> | |
<p>{description}</p> | |
</div> | |
<div className="button-container"> | |
<button className="button">edit</button> | |
<button className="button">delete</button> | |
</div> | |
</li> | |
); | |
} | |
export function ShowTodoList() { | |
const [todo, setTodo] = useState([]); | |
useEffect(() => { | |
axios | |
.get("http://localhost:8000/api/todo") | |
.then((res) => { | |
console.log(res.data); | |
setTodo(res.data); | |
}) | |
.catch((err) => { | |
console.log(err); | |
}); | |
}, []); | |
return ( | |
<section className="container"> | |
<Link to="/create-todo" className="button-new"> // added | |
<button className="button">New</button> | |
</Link> | |
<section className="contents"> | |
<h1>TODO</h1> | |
<ul className="list-container"> | |
{todo.map((data) => ( | |
<TodoCard data={data} /> | |
))} | |
</ul> | |
</section> | |
</section> | |
); | |
} |
.
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx
│ │ ├── showTodoList.jsx
│ │ └── updateTodo.jsx <-- we are here
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
useState
from react
axios
from axios
The
UpdateTodo
component will have 3 propsThe
updateTodo.jsx
file may look something like this.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/components/updateTodo.jsx | |
import { useState } from "react"; | |
import axios from "axios"; | |
export function UpdateTodo({ _id, handleClose, handleEdited }) { | |
const [data, setData] = useState({ title: "", description: "" }); | |
function handleChange(e) { | |
setData((data) => ({ ...data, [e.target.name]: e.target.value })); | |
} | |
function handleSubmit(e) { | |
e.preventDefault(); | |
console.log({ _id }, { data }); | |
axios | |
.put(`http://localhost:8000/api/todo/${_id}`, data) | |
.then((res) => { | |
setData({ title: "", description: "" }); | |
console.log(res.data.message); | |
}) | |
.catch((err) => { | |
console.log("Failed to update todo"); | |
console.log(err.message); | |
}); | |
} | |
return ( | |
<form | |
className="form-container" | |
onSubmit={(e) => { | |
handleSubmit(e); | |
handleEdited(); | |
handleClose(); | |
}} | |
> | |
<label htmlFor="title" className="label"> | |
Title | |
</label> | |
<input | |
type="text" | |
name="title" | |
className="input" | |
onChange={handleChange} | |
/> | |
<label htmlFor="description" className="label"> | |
Description | |
</label> | |
<input | |
type="text" | |
name="description" | |
className="input" | |
onChange={handleChange} | |
/> | |
<button type="submit" className="button"> | |
Submit | |
</button> | |
</form> | |
); | |
} |
.
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx
│ │ ├── showTodoList.jsx <-- we are here
│ │ └── updateTodo.jsx
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
We will make the following changes in
showTodoList.jsx
handleDelete
that will send a DELETE
request to the server. This function will need the _id
of the document to delete the document from the database. It will also update the array todo
with the filtered array.handleDelete
method as a prop to TodoCard
TodoCard
component to have the parameter handleDelete
onClick
event for the button delete
and pass the handleDelete
methodAfter making the changes, the code will look something like this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/components/showTodoList.jsx | |
import { useState, useEffect } from "react"; | |
import axios from "axios"; | |
import { Link } from "react-router-dom"; | |
function TodoCard({ data, handleDelete }) { | |
const { _id, title, description } = data; | |
return ( | |
<li key={_id}> | |
<div className="title-description"> | |
<h3>{title}</h3> | |
<p>{description}</p> | |
</div> | |
<div className="button-container"> | |
<button name={_id} className="button"> | |
edit | |
</button> | |
<button name={_id} className="button" onClick={handleDelete}> | |
delete | |
</button> | |
</div> | |
</li> | |
); | |
} | |
export function ShowTodoList() { | |
const [todo, setTodo] = useState([]); | |
useEffect(() => { | |
axios | |
.get("http://localhost:8000/api/todo") | |
.then((res) => { | |
console.log(res.data); | |
setTodo(res.data); | |
}) | |
.catch((err) => { | |
console.log(err); | |
}); | |
}, []); | |
function handleDelete(e) { | |
axios.delete(`http://localhost:8000/api/todo/${e.target.name}`); | |
setTodo((data) => { | |
return data.filter((todo) => todo._id !== e.target.name); | |
}); | |
} | |
return ( | |
<section className="container"> | |
<Link to="/create-todo" className="button-new"> | |
<button className="button">New</button> | |
</Link> | |
<section className="contents"> | |
<h1>TODO</h1> | |
<ul className="list-container"> | |
{todo.map((data) => ( | |
<TodoCard data={data} handleDelete={handleDelete} /> | |
))} | |
</ul> | |
</section> | |
</section> | |
); | |
} |
.
├── node_modules
├── public
├── src
│ ├── components
│ │ ├── createTodo.jsx
│ │ ├── showTodoList.jsx
│ │ └── updateTodo.jsx <-- we are here
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── index.js
│ ├── reportWebVitals.js
│ └── setupTests.js
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
└── yarn.lock
We need to add the following changes in the
showTodoList.jsx
UpdateTodo
component from updateTodo.jsx
open
using the useState
hook with the default value of false
. The value of open
will be either true
or false
. We will conditionally render the UpdateTodo
component. If the edit
button is clicked on any one of the todo then we will set open
to true
when the UpdateTodo
component will be rendered.id
using the useState
hook. The _id
of the todo document to be updated will be stored. It will be passed as a prop to UpdateTodo
component.update
using the useState
hook. This will be used to fetch all the todo documents from the database. Each time a todo document has been updated then update
will change between true
and false
handleEdit
. It will update the state id
with the _id
of the document and update the state of open
to true
. The UpdateTodo
component will be rendered.handleUpdate
. This will invert the state of update
if the todo has been updated by the user. Inverting the state will cause the useEffect
hook to update the todo
array.handleClose
. We need this to close the UpdateTodo
component. This will set id
to an empty string and set open
to false
.Update the
TodoCard
componenthandleEdit
function to the TodoCard
component.handleEdit
prop to the edit
button.After making the above changes, the code will look something like this
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/components/showTodoList.jsx | |
import { useState, useEffect } from "react"; | |
import axios from "axios"; | |
import { Link } from "react-router-dom"; | |
import { UpdateTodo } from "./updateTodo"; // added | |
function TodoCard({ data, handleEdit, handleDelete }) { // updated | |
const { _id, title, description } = data; | |
return ( | |
<li key={_id}> | |
<div className="title-description"> | |
<h3>{title}</h3> | |
<p>{description}</p> | |
</div> | |
<div className="button-container"> | |
<button className="button" name={_id} onClick={handleEdit}> // updated | |
edit | |
</button> | |
<button className="button" name={_id} onClick={handleDelete}> | |
delete | |
</button> | |
</div> | |
</li> | |
); | |
} | |
export function ShowTodoList() { | |
const [todo, setTodo] = useState([]); | |
const [open, setOpen] = useState(false); // added | |
const [id, setId] = useState(""); // added | |
const [update, setUpdate] = useState(false); // added | |
useEffect( | |
function () { | |
axios | |
.get("http://localhost:8000/api/todo") | |
.then((res) => { | |
console.log(res.data); | |
setTodo(res.data); | |
}) | |
.catch((err) => { | |
console.log(err.message); | |
}); | |
}, | |
[update] // updated | |
); | |
function handleEdit(e) { // added | |
setId(e.target.name); | |
setOpen(true); | |
} | |
function handleUpdate() { // added | |
console.log("update:", update, !update); | |
setUpdate(!update); | |
} | |
function handleDelete(e) { // added | |
axios.delete(`http://localhost:8000/api/todo/${e.target.name}`); | |
setTodo((data) => { | |
return data.filter((todo) => todo._id !== e.target.name); | |
}); | |
} | |
function handleClose() { // added | |
setId(""); | |
setOpen(false); | |
} | |
return ( | |
<section className="container"> | |
<Link to="/create-todo" className="button-new"> | |
<button className="button">New</button> | |
</Link> | |
<section className="contents"> | |
<h1>TODO</h1> | |
<ul className="list-container"> | |
{todo.map((data) => ( | |
<TodoCard | |
data={data} | |
handleEdit={handleEdit} | |
handleDelete={handleDelete} | |
/> | |
))} | |
</ul> | |
</section> | |
// added | |
{open ? ( | |
<section className="update-container"> | |
<div className="update-contents"> | |
<p onClick={handleClose} className="close"> | |
× | |
</p> | |
<UpdateTodo | |
_id={id} | |
handleClose={handleClose} | |
handleUpdate={handleUpdate} | |
/> | |
</div> | |
</section> | |
) : ( | |
"" | |
)} | |
</section> | |
); | |
} |
You can see the entire code for
part-2
in GitHub28