PS - This was originally posted on my blog. Check it out if you want learn more about React and JavaScript!
tl;dr - Clone and run the source code.
In the 2nd part of this series we're going to create a site with React to use with our Node API to create and view Notes. In the previous post we created the API for the app.
Prerequisites
- The Node API from the previous post must be up and running
- Setup the project following my guide
- A basic understanding of React hooks
Setup
First we need to setup the React project with a bundler. The bundler we're going to be using is Parcel, as it requires very little setup. Follow my guide to get started.
After you're done setting up React with Parcel, we'll be needing some additional dependencies.
yarn add axios formik react-icons
yarn add sass -D
-
axios
is used to make requests for the API -
formik
is used to make creating the new notes easier buy handling the forms -
react-icons
will be need for an icon for the delete note button -
sass
will be needed to compile the.scss
file we'll be using to style the app
Let's create an instance of axios
so that we don't have to enter the base URL for all network requests. In the src
folder create another folder services
and in that folder create the api.js
file and add the following code.
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:8080"
});
export default api;
We'll also need to change the font and title of the app. In index.html
add the link to the Rubik font files and a new title. Add these between <head>
and </head>
.
<link
href="https://fonts.googleapis.com/css?family=Rubik&display=swap"
rel="stylesheet"
/>
<title>Note App</title>
In the end src/index.html
should look like this.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<link
href="https://fonts.googleapis.com/css?family=Rubik&display=swap"
rel="stylesheet"
/>
<title>Note App</title>
</head>
<body>
<div id="root"></div>
<script src="index.js"></script>
</body>
</html>
Notes App
Now we can start working with the React part.
First first we need to figure out how we're going to store the notes list. We could use useState
to store the list, but we'll use useReducer
to simplify and bundle up all the different ways of updating the list.
In src/App.js
change the React import to
import React, { useReducer } from "react";
Then let's declare the initial state and reducer
const initialState = {
notesList: []
};
const reducer = (state, action) => {
let { notesList } = state;
switch (action.type) {
case "refresh":
notesList = [...action.payload];
break;
case "add":
notesList = [...notesList, action.payload];
break;
case "remove":
notesList = notesList.filter(note => note._id !== action.payload._id);
break;
}
return { notesList };
};
Initially we going to hold an empty array in the state. The reducer will have three actions, "refresh"
to get the list of notes when the app loads, "add"
to add a new note to the list, and "remove"
to delete a note. In the case of "add"
and "remove"
we could just refresh the whole list after doing them but that would be unnecessary and a waste of a network call.
To add the state to App
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
Next we need to load the list of notes when the app loads. We can do with the useEffect
hook. We'll need to import useEffect
and the axios
instance we created earlier.
import React, { useReducer, useEffect } from "react";
import api from "./services/api";
Add the following code before the return
in App
.
const getAllNotes = async () => {
try {
const response = await api.request({ url: "/note" });
dispatch({ type: "refresh", payload: response.data });
} catch (error) {
console.error("Error fetching notes", error);
}
};
useEffect(() => {
getAllNotes();
}, []);
All we're doing here is fetching the notes list as soon as the component mounts and updating the state using the reducer with "refresh"
. The second parameter of []
in useEffect
prevents this effect from running multiple times.
Now that we're loading the notes we need to display them. In return
, add the following
<main>
<h1>Notes App</h1>
{state.notesList.map(note => (
<div key={note._id} className="note">
<div className="container">
<h2>{note.title}</h2>
<p>{note.content}</p>
</div>
</div>
))}
</main>
We have no notes to load to load at the moment so let's add a footer to the page where we can create new notes.
First we need to import formik
which going to make handling the forms much easier.
import { Formik } from "formik";
Then let's add the UI and logic to create new note. Add this just after the <main>
tag.
<footer>
<Formik
initialValues={{ title: "", content: "" }}
validate={values => {
let errors = {};
if (!values.title) {
errors.title = "Title is required";
}
if (!values.content) {
errors.content = "Content is required";
}
return errors;
}}
onSubmit={async (values, { setSubmitting, resetForm }) => {
try {
const response = await api.request({
url: "/note",
method: "post",
data: {
title: values.title,
content: values.content
}
});
dispatch({ type: "add", payload: response.data });
resetForm();
} catch (error) {
console.error("Error creating note", error);
} finally {
setSubmitting(false);
}
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
}) => (
<form onSubmit={handleSubmit}>
<label for="title">Title</label>
<input
type="text"
name="title"
id="title"
onChange={handleChange}
onBlur={handleBlur}
value={values.title}
/>
{errors.title && touched.title && errors.title}
<br />
<label for="content">Content</label>
<textarea
rows={5}
name="content"
id="content"
onChange={handleChange}
onBlur={handleBlur}
value={values.content}
/>
{errors.content && touched.content && errors.content}
<br />
<button type="submit" disabled={isSubmitting}>
Create new note
</button>
</form>
)}
</Formik>
</footer>
formik
will handle all the values in the form including the validation and submitting to create the note.
Also we'll need some separation from main
and footer
so add this between them.
<hr />
Finally we need to be able to delete created notes, so we'll add a delete button to each note. First we need to add the delete function before the return
.
const removeNote = async id => {
try {
const response = await api.request({
url: `/note/${id}`,
method: "delete"
});
dispatch({ type: "remove", payload: response.data });
} catch (error) {
console.error("Error deleting note", error);
}
};
We'll need an icon for the delete note, so we'll import one from react-icons
.
import { FaTrash } from "react-icons/fa";
Then change the note component.
<div key={note._id} className="note">
<div className="container">
<h2>{note.title}</h2>
<p>{note.content}</p>
</div>
<button onClick={() => removeNote(note._id)}>
<FaTrash />
</button>
</div>
As the final part of the app let's add some styling. Create App.scss
in src
with the following code.
body {
font-family: "Rubik", sans-serif;
max-width: 800px;
margin: auto;
}
main {
.note {
display: flex;
flex-direction: row;
align-items: center;
.container {
display: flex;
flex-direction: column;
flex: 1;
}
button {
font-size: 1.5em;
border: 0;
background: none;
box-shadow: none;
border-radius: 0px;
}
button:hover {
color: red;
}
}
}
hr {
height: 1px;
width: 100%;
color: grey;
background-color: grey;
border-color: grey;
}
footer > form {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
input,
button,
textarea {
margin: 10px 0px 10px 0px;
font-family: "Rubik", sans-serif;
}
textarea {
resize: none;
}
}
Then import that in App.js
.
import "./App.scss";
Finally your App.js
should look like this.
// src/App.js
import React, { useReducer, useEffect } from "react";
import api from "./services/api";
import { Formik } from "formik";
import { FaTrash } from "react-icons/fa";
import "./App.scss";
const initialState = {
notesList: []
};
const reducer = (state, action) => {
let { notesList } = state;
switch (action.type) {
case "refresh":
notesList = [...action.payload];
break;
case "add":
notesList = [...notesList, action.payload];
break;
case "remove":
notesList = notesList.filter(note => note._id !== action.payload._id);
break;
}
return { notesList };
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const getAllNotes = async () => {
try {
const response = await api.request({ url: "/note" });
dispatch({ type: "refresh", payload: response.data });
} catch (error) {
console.error("Error fetching notes", error);
}
};
const removeNote = async id => {
try {
const response = await api.request({
url: `/note/${id}`,
method: "delete"
});
dispatch({ type: "remove", payload: response.data });
} catch (error) {
console.error("Error deleting note", error);
}
};
useEffect(() => {
getAllNotes();
}, []);
return (
<div>
<main>
<h1>Notes App</h1>
{state.notesList.map(note => (
<div key={note._id} className="note">
<div className="container">
<h2>{note.title}</h2>
<p>{note.content}</p>
</div>
<button onClick={() => removeNote(note._id)}>
<FaTrash />
</button>
</div>
))}
</main>
<hr />
<footer>
<Formik
initialValues={{ title: "", content: "" }}
validate={values => {
let errors = {};
if (!values.title) {
errors.title = "Title is required";
}
if (!values.content) {
errors.content = "Content is required";
}
return errors;
}}
onSubmit={async (values, { setSubmitting, resetForm }) => {
try {
const response = await api.request({
url: "/note",
method: "post",
data: {
title: values.title,
content: values.content
}
});
dispatch({ type: "add", payload: response.data });
resetForm();
} catch (error) {
console.error("Error creating note", error);
} finally {
setSubmitting(false);
}
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
}) => (
<form onSubmit={handleSubmit}>
<label for="title">Title</label>
<input
type="text"
name="title"
id="title"
onChange={handleChange}
onBlur={handleBlur}
value={values.title}
/>
{errors.title && touched.title && errors.title}
<br />
<label for="content">Content</label>
<textarea
rows={5}
name="content"
id="content"
onChange={handleChange}
onBlur={handleBlur}
value={values.content}
/>
{errors.content && touched.content && errors.content}
<br />
<button type="submit" disabled={isSubmitting}>
Create new note
</button>
</form>
)}
</Formik>
</footer>
</div>
);
};
export default App;
Running the app
Let's start the app by running the command
yarn dev
When you visit http://localhost:1234/
you should see
After you create the note, it should look like this
Top comments (0)