Build a Notes web app with React & Firebase

Build a Notes web app with React & Firebase

ยท

6 min read

Today, people have many activities to do, also a lot of great ideas or projects in their mind then we usually use our pencil to write those thoughts on paper quickly because they won't be lost easily. Unfortunately, we usually don't have a pencil or paper where we can put all ideas. Therefore, a notes app will be helpful.

Pre-requisites

  • Node.js (>= 14 version) installed.
  • Have a Firebase project (without any database) created.
  • Have basic React.js & Firebase knowledge.
  • Have basic Font Awesome knowledge.

Content

  1. Design the app
  2. Create our database in Cloud Firestore
  3. Create our React app and install some npm dependencies
  4. Prepare our React app with Firebase
  5. Create a hook for getting notes from Firebase
  6. Create NoteActionCard component
  7. Create NoteForm component
  8. Create NoteHeader component
  9. Create NoteList component
  10. Put the NoteList component in our app
  11. Demo
  12. Next steps

Design the app

Our notes app will have features like add a new note, delete a note, also use CSS Flexbox, Grid Layout, and integrate with Firebase to store our notes.

image.png

Create our database in Cloud Firestore

Cloud Firestore is a cloud-hosted, NoSQL database that your iOS, Android, and web apps can access directly via native SDKs - Google

image.png

  • Structure our database.

We'll create a collection notes with many documents (note).

image.png

For example:

[
    {
        "id": "6URepJtDX3tytzEzPqng",
        "text": "Write my article on Hashnode",
        "createdAt": "April 6, 2021 at 3:28:55 AM UTC-4"
    },
    {
        "id": "0AUaYnC3hm95NZ89WrBI",
        "text": "Call to my friend",
        "createdAt": "April 7, 2021 at 3:28:55 AM UTC-4"
    },
    {
        "id": "6LmlgsV54X9jmPgsT7Yi",
        "text": "Go to meeting on Saturday",
        "createdAt": "April 8, 2021 at 3:28:55 AM UTC-4"
    }
]

Create our React app and install some npm dependencies

npx create-react-app notes-app
cd notes-app
npm i firebase
npm i @fortawesome/react-fontawesome
npm i @fortawesome/fontawesome-svg-core
npm i @fortawesome/free-solid-svg-icons

Prepare our React app with Firebase

We need to create a new file where we'll put our Firebase configuration: firebaseConfig.js. (Note.- You need to replace your Firebase credentials here.)

// notes-app/src/firebaseConfig.js
import firebase from "firebase/app";
import "firebase/firestore";

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "notes-app-bedc0.firebaseapp.com",
  projectId: "notes-app-bedc0",
  storageBucket: "notes-app-bedc0.appspot.com",
  messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxxxxxx"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

const db = firebase.firestore();

export { db };

Create a hook for getting notes from Firebase

From React 16.8, we can use Hooks also we can create our custom hooks only we need to create a function with the prefix use.

Now, we create our custom hook useReadNotes for this purpose we'll "extract" some logic using hooks useState and useEffect to get information about notes from Firebase.

// notes-app/src/hooks/useReadNotes.js
import { db } from '../firebaseConfig'
const { useState, useEffect } = require("react");

function useReadNotes() {
    const [notes, setNotes] = useState([]);
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        const firebaseNotes = [];
        setLoading(true);
        db.collection("notes")
            .orderBy("createdAt", "desc")
            .get()
            .then((querySnapshot) => {
                querySnapshot.docs.forEach((doc) => {
                    firebaseNotes.push({
                        id: doc.id,
                        ...doc.data()
                    });
                });
                setNotes(firebaseNotes);
                setLoading(false);
            });
    }, []);

    return [loading, notes, setNotes];
}

export default useReadNotes;

Create NoteActionCard component

NoteActionCard will help us to open a small form where we can put information about a note.

image.png

// notes-app/src/components/NoteActionCard.jsx
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons'

const NoteActionCard = ({ setIsVisibleNew }) => {
    const openNewNoteForm = () => setIsVisibleNew(true);

    return (
        <div className="note note--empty">
            <p className="note__text note__text--add">Add new note</p>
            <button
                className="note__button note__button--new"
                onClick={openNewNoteForm}
            >
                <FontAwesomeIcon className="note__icon" icon={faPlus} />
            </button>
        </div>
    )
}

export default NoteActionCard;

Create NoteForm component

This component that we will use to enter information about a note.

image.png

// notes-app/src/components/NoteForm.jsx
import { db } from '../firebaseConfig'
import { useState } from "react"

const NoteForm = ({ setNotes, setIsVisibleNew, setSaving }) => {
    const [text, setText] = useState("");
    const handletext = (e) => setText(e.target.value);
    const cancelNote = () => setIsVisibleNew(false);

    const [error, setError] = useState(null);
    const saveNote = (e) => {
        e.preventDefault();
        setSaving(true);

        // Add a new document in collection "notes"
        let newNote = { text, createdAt: new Date() };
        db.collection("notes")
            .add({ text, createdAt: new Date() })
            .then(({ id }) => {
                newNote.id = id;
                setNotes(prevNotes => [newNote, ...prevNotes])
                setIsVisibleNew(false);
                setSaving(false);
            })
            .catch((error) => {
                console.error("Error writing document: ", error);
                setError(error);
            });
    }

    return (
        <div className="note note--create">
            <h2 className="note__heading">New Note</h2>
            <form onSubmit={saveNote} className="note__form">
                <div className="note__field">
                    <label
                        htmlFor="note__input"
                        className="note__label">Text for your note</label>
                    <input type="text"
                        className="note__input"
                        name="note__input"
                        id="note__input"
                        value={text}
                        onChange={handletext}
                        required
                    />
                </div>
                <div className="note__buttons">
                    <button type="submit"
                        className="note__button note__button--create">Create</button>
                    <button type="submit"
                        className="note__button note__button--cancel"
                        onClick={cancelNote}>Cancel</button>
                </div>
            </form>
            {error && <p className="note__message--error">{error}</p>}

        </div>
    )
}

export default NoteForm;

Create NoteHeader component

With NoteHeader we can delete a note (โŒ).

image.png

// notes-app/src/components/NoteHeader.jsx
import { db } from '../firebaseConfig'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons'

const NoteHeader = ({ id, setNotes }) => {
    const deleteNote = () => {

        db.collection("notes").doc(id).delete().then(() => {
            console.log("Note successfully deleted!");
            setNotes(prevNotes => prevNotes.filter(note => note.id !== id))
        }).catch((error) => {
            console.error("Error removing note: ", error);
        });

    };
    return (
        <div className="note__header">
            <button
                className="note__button"
                onClick={deleteNote}
            >
                <FontAwesomeIcon icon={faTimesCircle} />
            </button>
        </div>
    )
}

export default NoteHeader;

Create NoteList component

NodeList is a section where we will show all notes registered.

image.png

// notes-app/src/components/NoteList.jsx
import { useState } from "react";
import useReadNotes from "../hooks/useReadNotes"
import NoteActionCard from "./NoteActionCard";
import NoteForm from "./NoteForm";
import NoteHeader from './NoteHeader';

const NoteList = () => {
    const [loading, notes = [], setNotes] = useReadNotes();
    const [isVisibleNew, setIsVisibleNew] = useState(false);
    const [saving, setSaving] = useState(false);

    return (
        <section className="notes">
            <NoteActionCard
                setIsVisibleNew={setIsVisibleNew}
            />
            {isVisibleNew &&
                <>
                    <NoteForm
                        setNotes={setNotes}
                        setIsVisibleNew={setIsVisibleNew}
                        setSaving={setSaving}
                    />
                    {saving && <p className="note__message">Saving new note...</p>}
                </>
            }
            {loading &&
                <p className="notes__loading">Notes are loading...</p>
            }
            {
                notes.map(({ id, text = "" }) =>
                    <div className="note note--show" key={id} id={id}>
                        <NoteHeader
                            id={id}
                            setNotes={setNotes}
                        />
                        <div className="note__body">
                            <p className="note__text">{text}</p>
                        </div>
                    </div>
                )
            }
        </section>
    )
}

export default NoteList;

Put the NoteList component in our app

With all components built, we can use them in the main component App.

image.png

import './App.css';
import NoteList from './components/NoteList';

function App() {

  return (
    <div className="App">
      <h1>Notes</h1>
      <NoteList />
    </div>
  );
}

export default App;

Also, we need add our CSS styles:

@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap");
:root {
  --primary: #370617;
  --secondary: hsl(92, 24%, 70%);
  --tertiary: #e7b287;
  --accent: #081c15;
}

* {
  margin: 0;
  padding: 0;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

html {
  box-sizing: border-box;
}

body {
  background-color: var(--primary);
  color: var(--secondary);
  font-family: "Open Sans", sans-serif;
}

button {
  appearance: none;
  background-color: transparent;
  border: none;
  display: block;
  outline: none;
  text-align: center;
}

input:focus {
  outline: none;
}

h1,
h2 {
  font-family: "Roboto", sans-serif;
  font-weight: 700;
  text-align: center;
}

h1 {
  margin: 0.5em 0;
}

i {
  color: var(--secondary);
}

section {
  padding: 0 0.5em;
}

.notes {
  display: flex;
  flex-direction: column;
  gap: 0.5em;
}

.note {
  border-radius: 0.5em;
  color: var(--accent);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.5em;
  padding: 0.5em;
}

.note--create {
  background-color: var(--tertiary);
}

.note--empty {
  align-items: center;
  border: 1px dashed var(--secondary);
  color: var(--secondary);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.5em;
  padding: 4em;
}

.note--show {
  background-color: var(--secondary);
}

.note__body {
  padding: 4em;
}

.note__button {
  color: var(--accent);
}

.note__button--create {
  background-color: var(--primary);
  color: var(--secondary);
}

.note__button--create,
.note__button--cancel {
  border: 1px solid var(--primary);
  border-radius: 0.5em;
  padding: 0.5em;
  text-align: center;
  width: 100%;
}

.note__button--new {
  border-radius: 50%;
  color: var(--secondary);
  font-size: 1.2em;
  text-align: center;
  width: 2em;
}

.note__buttons {
  display: flex;
  flex-direction: column;
  gap: 0.2em;
}

.note__form {
  display: flex;
  flex-direction: column;
  gap: 0.5em;
}

.note__header {
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  gap: 0.5em;
}

.note__input {
  border: none;
  border-radius: 0.5em;
  padding: 0.5em;
  width: 100%;
}

.note__text {
  text-align: center;
}

@media (min-width: 350px) {
  .notes {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    margin: 0 auto;
  }
}

Demo

image.png

Next steps

  • Search notes by text.
  • Edit a note.

Thanks for your reading and time! I appreciate it.

See you until the next article!๐Ÿ˜„

ย