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.
Subscribe to my newsletter
Read articles from Henna Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Henna Singh
Henna Singh
Driven professional with expertise in problem-solving, relationship building, and event organization, complemented by a strong presence in blogging and public speaking. Currently studying Frontend Development Career Path from Scrimba and working as Program Manager at MongoDB. I am actively looking to switch to a more technical profile and build web solutions. I bring great leadership, program management and customer engineering background, that proves my strong interpersonal, team building and resourceful skills