DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Note App - Part 2: The React Site
Akhila Ariyachandra
Akhila Ariyachandra

Posted on • Updated on • Originally published at akhilaariyachandra.com

Note App - Part 2: The React Site

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
Enter fullscreen mode Exit fullscreen mode
yarn add sass -D
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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();
}, []);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 />
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

We'll need an icon for the delete note, so we'll import one from react-icons.

import { FaTrash } from "react-icons/fa";
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then import that in App.js.

import "./App.scss";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Running the app

Let's start the app by running the command

yarn dev
Enter fullscreen mode Exit fullscreen mode

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

First Preview

After you create the note, it should look like this

Second Preview

Top comments (0)

Interested in expanding your horizon for programming languages, but not sure where to start?

Check out this great DEV post:

Hello world in 10 different languages