Build a Notes App with JavaScript localStorage
Build a notes app with plain JavaScript that lets a user create, edit, delete, and permanently save notes using localStorage.
A JavaScript notes app lets a user write a note, save it, edit it later, and delete it when it is no longer needed, with every note still there after closing and reopening the page. It builds directly on the array-and-render pattern from a todo list, but adds editing, which means updating one specific item in place instead of only adding or removing.
You will build a form for creating notes, a list that displays them, and edit and delete controls on each note. Every change is saved with localStorage so nothing is lost on refresh.
What You Will Build
The app needs to support:
- Creating a note from a title and content input.
- Displaying every saved note in a list.
- Editing an existing note's content.
- Deleting a note.
- Keeping every note saved between visits.
Step 1: Build the HTML
Start with a form for new notes and an empty container for the list.
<form id="noteForm">
<input type="text" id="noteTitle" placeholder="Title" />
<textarea id="noteContent" placeholder="Write a note..."></textarea>
<button type="submit">Save Note</button>
</form>
<div id="notesList"></div>The list container starts empty. JavaScript will build one block of markup per saved note whenever the list changes, the same rebuild-from-data approach used in a todo list app.
Step 2: Store Notes as an Array of Objects
Decide the shape of a note before writing any DOM code.
const form = document.getElementById("noteForm");
const titleInput = document.getElementById("noteTitle");
const contentInput = document.getElementById("noteContent");
const notesList = document.getElementById("notesList");
let notes = [];Each note will be an object with an id, a title, and content. The id is what makes editing possible later, since it lets a specific note be found and updated without touching any of the others.
Step 3: Render the Notes List
Write one function that clears the list and rebuilds it from the current notes array, including an edit and delete button on each note.
function createActionButton(label, noteId, action) {
const button = document.createElement("button");
button.textContent = label;
button.dataset.id = noteId;
button.dataset.action = action;
return button;
}This small helper builds one button with a label, the note's id, and an action name, since the edit and delete buttons only differ by those three values.
function createNoteCard(note) {
const card = document.createElement("div");
const title = document.createElement("h4");
title.textContent = note.title;
const content = document.createElement("p");
content.textContent = note.content;
card.append(
title,
content,
createActionButton("Edit", note.id, "edit"),
createActionButton("Delete", note.id, "delete")
);
return card;
}The title and content are set with textContent instead of being inserted as an HTML string, since they come directly from what the user typed. Using textContent treats that text as plain text rather than markup, which matters if a note ever contains characters like < or &.
function renderNotes() {
notesList.innerHTML = "";
notes.forEach((note) => notesList.appendChild(createNoteCard(note)));
}renderNotes clears the list and appends a fresh card for every note in the array, built by the helpers above. Every other function in this app calls renderNotes after it changes the array, so the page never falls out of sync with the real data.
Step 4: Create and Delete Notes
Creating a note responds to the form submission, and deleting responds to a click on the list.
form.addEventListener("submit", (event) => {
event.preventDefault();
notes.push({
id: Date.now(),
title: titleInput.value.trim(),
content: contentInput.value.trim(),
});
form.reset();
saveNotes();
renderNotes();
});Submitting the form adds a new note object to the array using the current time as a simple id, clears the form, saves, and re-renders. Deleting uses one listener on the list container instead of one per button.
notesList.addEventListener("click", (event) => {
const id = Number(event.target.dataset.id);
if (event.target.dataset.action === "delete") {
notes = notes.filter((note) => note.id !== id);
saveNotes();
renderNotes();
}
});This single listener reads which note and which action the click belongs to, then filters the matching note out of the array before saving and re-rendering. Handling every click this way is a pattern called event delegation, covered in more depth in the JavaScript event delegation guide.
Step 5: Edit an Existing Note
Editing is the one action that updates a note in place instead of adding or removing one.
notesList.addEventListener("click", (event) => {
const id = Number(event.target.dataset.id);
if (event.target.dataset.action === "edit") {
const newContent = prompt("Edit your note:");
if (newContent === null) return;
notes = notes.map((note) =>
note.id === id ? { ...note, content: newContent } : note
);
saveNotes();
renderNotes();
}
});This handler asks for new content with a simple prompt, then builds a new array where only the matching note gets its content replaced. Every other note in the array stays exactly as it was, which is why matching on id matters so much in this app.
Step 6: Save and Load Notes
Saving after every change makes the notes app remember its data between visits.
function saveNotes() {
localStorage.setItem("notes", JSON.stringify(notes));
}
function loadNotes() {
const saved = localStorage.getItem("notes");
notes = saved ? JSON.parse(saved) : [];
renderNotes();
}
loadNotes();Saving converts the notes array to a string before writing it to storage, and loading reverses that conversion when the page starts. For more on storing structured data like this, see the storing complex objects in localStorage guide.
Common Mistakes
| Mistake | Why it breaks | Fix |
|---|---|---|
| Editing a note by searching the DOM instead of the array | The change is lost the next time the list re-renders | Update the matching object in the array first, then re-render |
| Forgetting to save after editing or deleting | Refreshing the page brings back the old, unsaved version | Call the save function after every change, not just after creating a note |
| Using array position instead of an id to find a note | Deleting or reordering notes shifts positions and targets the wrong one | Always match on a stable id, never on array index |
Next Step
You now have the full create, edit, delete, and save pattern used across most small JavaScript apps. If you have not built the JavaScript calculator yet, it is a good next project for practicing state tracking without a list involved.
Rune AI
Key Insights
- Store each note as an object with an id, title, and content, not as a single block of text.
- Use one render function that rebuilds the notes list from the array after every change.
- Editing a note means updating the matching object in the array, then saving and re-rendering.
- Save the notes array to localStorage after every change so nothing is lost on refresh.
- Load saved notes once when the page starts so the list is ready immediately.
Frequently Asked Questions
Why does each note need its own id?
Is localStorage a good place to store notes long term?
Why re-render the whole notes list instead of updating one note in place?
Conclusion
A notes app combines everything from the earlier mini projects: an array of objects as the source of truth, a render function that rebuilds the page from that array, and localStorage to make changes permanent. This create, edit, delete, and save pattern is the foundation of most small data-driven apps you will build next.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.