Note App - Part 2: The React Site

- 74 views

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

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

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.

src/services/api.js
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>.

src/index.html
<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.

src/index.html
<!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

src/App.js
import React, { useReducer } from "react";

Then let’s declare the initial state and reducer

src/App.js
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

src/App.js
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.

src/App.js
import React, { useReducer, useEffect } from "react";
import api from "./services/api";

Add the following code before the return in App.

src/App.js
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

src/App.js
<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.

src/App.js
import { Formik } from "formik";

Then let’s add the UI and logic to create new note. Add this just after the <main> tag.

src/App.js
<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.

src/App.js
<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.

src/App.js
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.

src/App.js
import { FaTrash } from "react-icons/fa";

Then change the note component.

src/App.js
<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.

src/App.scss
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.

src/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

Result 1

After you create the note, it should look like this

Result 2