React: Notes App

In this article, we will make changes to the pre-built React Notes App.

Improvements to Make:

  • Sync Notes with Local Storage

  • Add note summary titles

  • Move the modified note to the top of the list

  • Delete notes

  • Saving Notes to Firebase Database

App in Action

Using localStorage

localStorage.getItem("key")
localStorage.setItem("key", value)
//value must be a string. If its a more complex value like an array or 
//object to save, you'll need to use:
JSON.stringify(value)
JSON.parse(stringifiedValue)

More on MDN docs: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage

We can use useEffect() hook as notes are changing at multiple places to save it to the local storage

const [notes, setNotes] = React.useState(
JSON.parse(localStorage.getItem("notes")) || []

React.useEffect(() => {
localStorage.setItem("notes", JSON.stringify(notes))
}, [notes])

Lazy State Initialization

With Lazy State Initialization, the code is rendered only once and the state is not initialized on every app re-render. For database or any other expensive operations, it is best to use lazy state initialization so the rendering happens only the first time the app is rendered.

    const [notes, setNotes] = React.useState(
        () => JSON.parse(localStorage.getItem("notes")) || []
    )

Adding Note Summary

To display the first line of the note body as a note heading, we can use split method on newline delimiter

 <h4 className="text-snippet">{note.body.split("\n")[0]}</h4>

Bumping recent note to the top

    function updateNote(text) {
        // Try to rearrange the most recently-modified
        // note to be at the top
        setNotes(oldNotes => {
            // Create a new empty array
            // Loop over the original array
                // if the id matches
                    // put the updated note at the 
                    // beginning of the new array
                // else
                    // push the old note to the end
                    // of the new array
            // return the new array
              const newArray = []
              for(let i = 0; i < oldNotes.length; i++) {
              const oldNote = oldNotes[i]
              if(oldNote.id === currentNoteId) {
                 newArray.unshift({ ...oldNote, body: text })
              } else {
                 newArray.push(oldNote)
              }
            }
            return newArray
        })

        // This current code does not rearrange the notes
        // setNotes(oldNotes => oldNotes.map(oldNote => {
        //     return oldNote.id === currentNoteId
        //         ? { ...oldNote, body: text }
        //         : oldNote
        // }))
    }

Deleting Notes

For deleting the notes, we are trying to pass 2 variables, event and a noteid

    function deleteNote(event, noteId) {
        event.stopPropagation()
        setNotes(oldNotes => oldNotes.filter(note => note.id !== noteId))
    }

The two variables are passed in the Sidebar component in the following way:

    const noteElements = props.notes.map((note, index) => (             
           <button 
               className="delete-btn"
               onClick={(event) => props.deleteNote(event, note.id)}
               >
           <i className="gg-trash trash-icon"></i>
           </button>
    ))

//App.js

             <Sidebar
                    notes={notes}
                    currentNote={findCurrentNote()}
                    setCurrentNoteId={setCurrentNoteId}
                    newNote={createNewNote}
                    deleteNote={deleteNote}
                />

The full code can be checked in the following sandbox code

Update to Notes App - Adding Firebase 🔥

Login to the Firebase console and create a project. Once the project is created, you can create a web app by clicking </> with the same name

Once you register the app, it will ask you to add Firebase SDK. Copy the code to the clipboard

Continue to console and create a Cloud Firestore database. The security rules are important as they control who can write to your DB.

Follow the instructions on the screen and enable Firestore. Create a firebase.js file on the local project and copy the code on the clipboard to the file.

After a few more imports, the firebase.js file is as below:

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore"

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyAftHqQ8l6GahZB5DpTnRO_YmtaIsnh4Z4",
  authDomain: "notesapp-251123.firebaseapp.com",
  projectId: "notesapp-251123",
  storageBucket: "notesapp-251123.appspot.com",
  messagingSenderId: "66167782460",
  appId: "1:66167782460:web:0bf7aa181bf896087cd739"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app)
const notesCollection = collection(db, "notes")

In our Notes application, there are 2 copies of data, one that is maintained locally, and the other that will be stored in the cloud firestore. To keep both copies in sync, firebase provides onSnapshot function.

This function listens to changes in the firstore database and act accordingly in the local code. This means if a delete request is sent to the database, when the database correctly reflects the delete request, the onSnapshot will update the copy of the database and changes can be made to the local code in the callback function provided by onSnapshot.

The function takes two parameters, the collection we want to listen to and the callback function which is called whenever notesCollection changes. The variable snapshot contains the most updated version of the database.

React.useEffect() {
    onSnapshot(notesCollection, function(snapshot) {
     // Sync up our local notes array with the snapshot data
    })
},[])

We are creating a web-socket connection with the database by setting onSnapshot listener. It is best practice to unmount the listener when the app closes to avoid memory leaks.

    React.useEffect(() => {
        const unsubscribe = onSnapshot(notesCollection, function(snapshot) {
            // Sync up our local notes array with the snapshot data

        })
        //clean up side-effects
        return unsubscribe
    }, [])

Every database that you use will provide its own way of generating unique ids. Firebase Cloustore also provides an id that will be used in the application

    React.useEffect(() => {
        const unsubscribe = onSnapshot(notesCollection, function(snapshot) {
            // Sync up our local notes array with the snapshot data
            const notesArr = snapshot.docs.map(doc => ({
                ...doc.data(),
                id: doc.id
            }))
            setNotes(notesArr)
        })
        return unsubscribe
    }, [])

For details on Firebase functions: https://firebase.google.com/docs/firestore/query-data/listen

Creating New Note

uses addDoc() firebase function that takes two parameters, the collection and the newNote to be added. It returns a promise

    async function createNewNote() {
        const newNote = {
            body: "# Type your markdown note's title here"
        }
        const newNoteRef = await addDoc(notesCollection, newNote)
        setCurrentNoteId(newNoteRef.id)
    }

Deleting a Note

uses doc firebase method and takes 3 parameters

    async function deleteNote(noteId) {
        const docRef = doc(db,  "notes", noteId)
        await deleteDoc(docRef)
    }

Updating a Note

is a much smaller function now than what we had previously. In the current collection, there is only 1 field, so updating the whole document is not expensive. In larger documents, only the updated field should be changed, that is achieved by specifying merge: true

    async function updateNote(text) {
        const docRef = doc(db, "notes", currentNoteId)
        await setDoc(docRef, {body: text, updatedAt: Date.now()}, {merge: true})
    }

Bumping updated note to the top

Two new fields are added to the collection, createdAt and updatedAt and the array is sorted based on updatedAt field. All changes in App.jsx

 const sortedNotes = notes.sort((a, b) => b.updatedAt - a.updatedAt)

    async function createNewNote() {
        const newNote = {
            body: "# Type your markdown note's title here",
            createdAt: Date.now(),
            updatedAt: Date.now()
        }
        const newNoteRef = await addDoc(notesCollection, newNote)
        setCurrentNoteId(newNoteRef.id)
    }

    async function updateNote(text) {
        const docRef = doc(db, "notes", currentNoteId)
        await setDoc(docRef, {body: text, updatedAt: Date.now()}, {merge: true})
    }

     <Sidebar
         notes={sortedNotes}
        .....
     />

Debouncing Updates

Firebase has a limit to the number of writes and reads you can do on a free tier. To control the number of write requests sent out to Firebase, we can set a timer to delay them.

  • Delay the request for a specified amount of time (eg 500ms)

  • If another request happens within the specified time, cancel the previous request and set up a new delay for the new request.

Make changes to a tempNote and update Editor references to tempNotes

//App.jsx
    const [tempNoteText, setTempNoteText] = React.useState("")

    React.useEffect(() => {
        if (currentNote) {
            setTempNoteText(currentNote.body)
        }
    }, [currentNote])

    <Editor
       tempNoteText={tempNoteText}
       setTempNoteText={setTempNoteText}
    />

//Editor.jsx
export default function Editor({ tempNoteText, setTempNoteText }) {

    return (
        <section className="pane editor">
            <ReactMde
                value={tempNoteText}
                onChange={setTempNoteText}
                ....
            />
        </section>
    )
}

The Debouncing logic can be written as below:

    React.useEffect(() => {
        const timeoutId = setTimeout(() => {
            if (tempNoteText !== currentNote.body) {
                updateNote(tempNoteText)
            }
        }, 500)
        return () => clearTimeout(timeoutId)
    }, [tempNoteText])

Please note, that you will have to create your own firestore db to make updates as only I can make updates on mine.