In short
A notes app looks simple, but it teaches the full CRUD loop (create, read, update, delete), plus making data survive a refresh.
A CRUD notes app with create, edit, delete and search — a compact project that exercises component composition and state updates cleanly.

Think of it like this
The back pages of your register
Every student scribbles reminders at the back of a notebook: add a line, cross one out, fix a spelling. Your app does the same, except it also has to remember everything after the notebook, your browser tab, is closed and reopened.
Watch it happen
Life of a new note
add("Buy chart paper")
1/5You type a note and press Add. The editor calls add from useNotes.
The big ideas
What you need to know
Full CRUD
Add, list, edit and delete notes, all from one notes array in state.
Persist to localStorage
Save the array whenever it changes and load it on start, so notes survive refreshes.
Search and sort
Filter notes with a search box and show the most recently edited first.
Stable IDs
Give every note an id from crypto.randomUUID() so edits and deletes always hit the right one.
How it works
Build plan
- 1
Add and list
An input and a button that add new notes to state and show them.
- 2
Edit and delete
Buttons on each note that update the array without mutating it.
- 3
Persist
Load from and save to localStorage with a custom hook.
- 4
Polish
Add search, timestamps and a friendly empty-state message.
See it in code
Walk through the code
1import { useState } from "react";2 3type Note = { id: string; text: string; updatedAt: number };4 5export function useNotes() {6 const [notes, setNotes] = useState<Note[]>([]);7 8 const add = (text: string) =>9 setNotes((n) => [{ id: crypto.randomUUID(), text, updatedAt: Date.now() }, ...n]);10 const update = (id: string, text: string) =>11 setNotes((n) => n.map((x) => (x.id === id ? { ...x, text, updatedAt: Date.now() } : x)));12 const remove = (id: string) => setNotes((n) => n.filter((x) => x.id !== id));13 14 return { notes, add, update, remove };15}1/4Each note has a stable id, its text and when it was last edited.
All three updates build new arrays instead of changing the old one, so React always re-renders. Swap useState for a useLocalStorage hook to keep notes after a refresh.
Save yourself the pain
Common mistakes
Oops
Using the array index as a note's id or key.
Do this instead
Indexes shift when you delete a note, so the wrong one gets edited. Use crypto.randomUUID().
Oops
Saving to localStorage separately inside every button handler.
Do this instead
Save in one useEffect that watches notes. One place, no forgotten saves.
Your mission
First milestone: notes that survive a refresh
Quick check
Your note list uses the array index as key. You delete note 2 of 5. What can go wrong?
Go deeper
- 1Model notes as an array of objects in state, with unique IDs.
- 2Add search/filter functionality over the notes list.
- 3Persist notes to localStorage or a simple backend API.
More projects at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.