How to build a notes app in React
A notes application is an ideal project for mastering React state management, CRUD operations, and data persistence. As the creator of CoreUI with over 10 years of React experience since 2014, I’ve built note-taking interfaces for knowledge management tools, CMS editors, and productivity dashboards. The most effective approach uses useState for notes, useEffect for localStorage persistence, and controlled inputs for editing. This delivers a fully functional notes app with minimal complexity.
Create the core notes state and display structure.
import { useState, useEffect } from 'react'
function NotesApp() {
const [notes, setNotes] = useState(() => {
const saved = localStorage.getItem('notes')
return saved ? JSON.parse(saved) : []
})
const [activeId, setActiveId] = useState(null)
useEffect(() => {
localStorage.setItem('notes', JSON.stringify(notes))
}, [notes])
const createNote = () => {
const note = { id: Date.now(), title: 'New Note', body: '', updatedAt: Date.now() }
setNotes([note, ...notes])
setActiveId(note.id)
}
const deleteNote = (id) => {
setNotes(notes.filter(n => n.id !== id))
if (activeId === id) setActiveId(null)
}
const activeNote = notes.find(n => n.id === activeId)
return (
<div className="notes-app">
<aside className="sidebar">
<button onClick={createNote}>+ New Note</button>
<ul>
{notes.map(note => (
<li
key={note.id}
className={note.id === activeId ? 'active' : ''}
onClick={() => setActiveId(note.id)}
>
<strong>{note.title || 'Untitled'}</strong>
<button onClick={(e) => { e.stopPropagation(); deleteNote(note.id) }}>×</button>
</li>
))}
</ul>
</aside>
<main>
{activeNote ? (
<NoteEditor note={activeNote} setNotes={setNotes} notes={notes} />
) : (
<p>Select a note or create a new one</p>
)}
</main>
</div>
)
}
Notes load from localStorage on mount via the useState initializer. The activeId tracks which note is open. New notes are prepended so the latest appears first. Deleting the active note clears the editor.
Building the Note Editor
Create an editor that updates notes as you type.
function NoteEditor({ note, notes, setNotes }) {
const updateNote = (field, value) => {
setNotes(notes.map(n =>
n.id === note.id
? { ...n, [field]: value, updatedAt: Date.now() }
: n
))
}
const formatDate = (timestamp) =>
new Date(timestamp).toLocaleString('en-US', {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
})
return (
<div className="note-editor">
<input
className="note-title"
type="text"
value={note.title}
onChange={(e) => updateNote('title', e.target.value)}
placeholder="Note title"
/>
<p className="note-meta">Last updated: {formatDate(note.updatedAt)}</p>
<textarea
className="note-body"
value={note.body}
onChange={(e) => updateNote('body', e.target.value)}
placeholder="Start writing..."
/>
</div>
)
}
updateNote patches the changed field while preserving other fields. It also updates the timestamp. Changes persist to localStorage via the parent’s useEffect. The editor is fully controlled.
Adding Search Functionality
Filter notes by title and content.
function NotesApp() {
const [notes, setNotes] = useState(() => {
const saved = localStorage.getItem('notes')
return saved ? JSON.parse(saved) : []
})
const [activeId, setActiveId] = useState(null)
const [search, setSearch] = useState('')
useEffect(() => {
localStorage.setItem('notes', JSON.stringify(notes))
}, [notes])
const filteredNotes = notes.filter(note =>
note.title.toLowerCase().includes(search.toLowerCase()) ||
note.body.toLowerCase().includes(search.toLowerCase())
)
return (
<div className="notes-app">
<aside className="sidebar">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search notes..."
/>
<ul>
{filteredNotes.map(note => (
<li key={note.id} onClick={() => setActiveId(note.id)}>
{note.title || 'Untitled'}
</li>
))}
</ul>
</aside>
</div>
)
}
filteredNotes filters in real time as search changes. Both title and body are searched. The original notes array stays unchanged so clearing the search restores the full list.
Sorting Notes
Sort by last updated or alphabetically.
const [sortBy, setSortBy] = useState('updated')
const sortedNotes = [...filteredNotes].sort((a, b) => {
if (sortBy === 'updated') return b.updatedAt - a.updatedAt
return a.title.localeCompare(b.title)
})
Sorting creates a new array to avoid mutating state. Date sort puts recently edited notes first. Title sort uses localeCompare for correct alphabetical order. Add a dropdown to let users switch between sort modes.
Best Practice Note
This is the same notes architecture we use in CoreUI content management templates. For production apps, replace localStorage with an API backend so notes sync across devices. Add Markdown rendering with a library like react-markdown for rich formatting. Implement auto-save with a debounced useEffect to avoid excessive writes on every keystroke. Add keyboard shortcut Ctrl+N for creating new notes quickly. Consider tags or folders for organization as the notes collection grows.



