Note App - Part 2: The React Site
Tags: react, parcel, javascript
Published: 2019-07-12 02:21:32

> PS - This was originally posted on my [blog](https://akhilaariyachandra.com/note-app-part-2-setup-the-react-site/ "Note App - Part 2: The React Site"). Check it out if you want learn more about React and JavaScript!

**tl;dr** - Clone and run the [source code](https://github.com/akhila-ariyachandra/note-web "Note Web").

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](https://akhilaariyachandra.com/post/note-app-part-1-setup-the-node-api "Note App - Part 1: Setup the Node API") we created the API for the app.

# Prerequisites

- The Node API from the previous [post](https://akhilaariyachandra.com/post/note-app-part-1-setup-the-node-api "Note App - Part 1: Setup the Node API") must be up and running
- Setup the project following my [guide](https://akhilaariyachandra.com/post/getting-started-in-react-with-parcel-js "Getting started in React with Parcel.js")
- A basic understanding of [React hooks](https://reactjs.org/docs/hooks-intro.html)

# Setup

First we need to setup the React project with a bundler. The bundler we're going to be using is [Parcel](https://parceljs.org/), as it requires very little setup. Follow my [guide](https://akhilaariyachandra.com/post/getting-started-in-react-with-parcel-js "Getting started in React with Parcel.js") to get started.

After you're done setting up React with Parcel, we'll be needing some additional dependencies.

```shell
yarn add axios formik react-icons
```

```shell
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.

```javascript
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>`.

```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.

```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

```jsx
import React, { useReducer } from "react";
```

Then let's declare the initial state and reducer

```jsx
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`

```jsx
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.

```jsx
import React, { useReducer, useEffect } from "react";
import api from "./services/api";
```

Add the following code before the `return` in `App`.

```jsx
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

```jsx
<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.

```jsx
import { Formik } from "formik";
```

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

```jsx
<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.

```jsx
<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`.

```jsx
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`.

```jsx
import { FaTrash } from "react-icons/fa";
```

Then change the note component.

```jsx
<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.

```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`.

```jsx
import "./App.scss";
```

Finally your `App.js` should look like this.

```jsx
// 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

```shell
yarn dev
```

When you visit `http://localhost:1234/` you should see

![First Preview](https://res.cloudinary.com/dv1ustbp5/image/upload/v1562848897/blog/note%20app%20part%202/Running_the_app_wcxyxe.png)

After you create the note, it should look like this

![Second Preview](https://res.cloudinary.com/dv1ustbp5/image/upload/v1562849314/blog/note%20app%20part%202/note_preview_vuh5hp.png)