DAY 40: How I Built a Notes App in React with Zustand State Management | ReactJS


π Introduction
Welcome to Day 40 of my Web Development Journey!
After building a solid foundation in HTML, CSS, and JavaScript, Iβve been diving into ReactJS β a powerful library for creating dynamic and interactive user interfaces.
Over the past few days, after learning React core concepts and Zustand for state management, I decided to build a Notes App. I chose this project to practice CRUD operations, global state management, form validation, and building a responsive UI. This project covers almost all the core concepts of React and gave me hands-on experience with global state management using Zustand.
You can explore the full source code on Notes App GitHub Repository if you want to see the complete project.
To stay consistent and share my learnings, Iβm documenting this journey publicly. Whether youβre just starting with React or need a refresher, I hope this blog provides something valuable!
Feel free to follow me on Twitter for real-time updates and coding insights.
π Hereβs What I Covered Over the Last 3 Days
Day 37
- Started building the Notes App
- Set up folder structure
- Installed Zustand
- Built UI components
- Styled using Tailwind CSS
Day 38
- Created a modal for adding notes
- Used createPortal
- Implemented form input validation
- Created
useNotesStore
for managing notes - Added notes to the store after validation
Day 39
- Displayed all notes on the UI
- Implemented delete, edit, and toggle functionality
- Added search feature
- Implemented filter by category and filter by completed
- Stored all notes in localStorage using the persist middleware
This project allowed me to strengthen my React skills, understand state management with Zustand, and practice building a user-friendly and interactive app.
Letβs dive into these concepts and the app in more detail below π
1. Notes App:
After building a solid foundation in React and experimenting with state management using Zustand, I wanted to create a practical application to practice adding, editing, filtering, and managing notes efficiently. This inspired me to build a Notes App β a dynamic, interactive tool for organizing our personal and professional notes.
Project Overview:
This Notes App enables users to:
- Add new notes with a title, category, and optional description
- Edit existing notes with pre-filled data for smooth editing
- Mark notes as completed or incomplete
- Filter notes by category (Personal, Home, Business)
- Search notes by title
- Show only completed notes using a toggle checkbox
- Persist all notes in localStorage using Zustandβs persist middleware
- Enjoy a responsive and intuitive UI with Tailwind CSS styling
The app is built entirely with React functional components, hooks, and Zustand for global state management.
Core Features:
Here are the main functionalities and features implemented in this Notes App project:
Note Management
- Add Notes: Users can add new notes via a modal form. Validation ensures the title is required (min 2 characters) and a category is selected.
- Edit Notes: Edit mode pre-fills the form with existing note data. Changes are saved globally in the store.
- Toggle Completion: Checkbox allows marking notes as completed or pending.
- Delete Notes: Notes can be removed individually.
Filtering & Search
- Category Tabs: Switch between ALL, PERSONAL, HOME, and BUSINESS tabs to filter notes by category.
- Search Functionality: Real-time search by note title using the search input in the header.
- Show Completed Toggle: Filter to display only completed notes.
State & Validation
- Zustand Store: Global state management using
useNotesStore
,useModalStore
, anduseNoteFormStore
. - Validation: Custom
useNoteForm
hook validates form input based on rules for title and category. - Error Handling: Inline error messages display invalid inputs in the modal form.
Data Persistence
- Persisted Storage: Notes are stored in
localStorage
via Zustand'spersist
middleware, maintaining data across sessions.
User Interface
- Modal Form: Clicking the βAddβ button opens a modal for adding or editing notes.
- Focus Management: Input fields auto-focus in edit mode and when modal opens.
- Visual Feedback: Completed notes are grayed out with strikethrough titles and descriptions.
- Responsive Controls: Tabs, checkboxes, and buttons are clearly styled with Tailwind CSS.
React Concepts Applied:
useState
β for managing search query, active tab, filter toggles, and description countuseEffect
β for handling escape key press and auto-focus of input fieldsuseRef
β for managing input focus inside modaluseId
β generating unique IDs for form elements- Zustand Stores β managing notes, modal state, and note form state globally
createPortal
β rendering the modal outside the root DOM hierarchy- Custom Hooks:
useEscapeClose
β closes modal on Escape key pressuseNoteForm
β handles form validation, adding/editing notes, and resetting fields
Folder Structure:
src/
βββ App.jsx
βββ assets/
β βββ empty-notes-icon.svg
β βββ search.svg
βββ components/
β βββ AddNoteModal.jsx
β βββ Header.jsx
β βββ Home.jsx
β βββ NotesCard.jsx
β βββ NotesList.jsx
β βββ NotesStatus.jsx
βββ hooks/
β βββ useEscapeClose.js
β βββ useNoteForm.js
βββ stores/
β βββ useModalStore.js
β βββ useNoteFormStore.js
β βββ useNotesStore.js
βββ utils/
β βββ getFormattedDate.js
Code Walkthrough:
Letβs break down how the Notes App works, component by component, including the key functionality and state management logic.
1. App.jsx β Root Component
The App
component is the entry point of the app. It manages the search query state and renders the Header
and Home
components.
const App = () => {
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="font-roboto min-h-screen bg-[#EEEEEE]">
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
<Home searchQuery={searchQuery} />
</div>
);
};
- The
searchQuery
state is passed down to bothHeader
andHome
components. - This setup allows the search input in the header to filter notes in real-time across the app.
2. Header.jsx β Search & Add Note
The Header
component serves two main purposes: searching notes and adding new notes.
const Header = ({ searchQuery, setSearchQuery }) => {
const openModal = useModalStore((state) => state.openModal);
return (
<header>
{/* Search Bar */}
<input
type="text"
placeholder="Search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{/* Add Note Button */}
<button onClick={() => openModal()}>Add</button>
{/* Add Note Modal */}
<AddNoteModal />
</header>
);
};
Search Functionality The input field value is controlled by
searchQuery
.
On change, it updates thesearchQuery
state in theApp
component, enabling real-time note filtering.Add Note Modal The Add button triggers
openModal
fromuseModalStore
, toggling the modal state.
TheAddNoteModal
component is rendered inside the header to show a modal for creating or editing notes.Zustand for Global State
useModalStore
manages the modal's open/close state globally, making it easy to control the modal from any component.
3. AddNoteModal.jsx β Add & Edit Notes
This modal handles both adding new notes and editing existing ones.
const { isModalOpen, closeModal } = useModalStore(
useShallow((state) => ({
isModalOpen: state.isModalOpen,
closeModal: state.closeModal,
})),
);
const { noteInput, setNoteInput, editingId } = useNoteFormStore(
useShallow((state) => ({
noteInput: state.noteInput,
setNoteInput: state.setNoteInput,
editingId: state.editingId,
})),
);
const {
id,
titleInputRef,
errors,
descCount,
setDescCount,
handleAddNote,
resetFields,
resetForm,
} = useNoteForm();
useEscapeClose(isModalOpen, closeModal, titleInputRef);
if (!isModalOpen) return null;
Key Features
- Title & Category Validation: Uses
useNoteForm
hook to validate inputs before submission. - Description Character Count: Tracks the number of characters in the description in real-time.
- Edit Mode: If
editingId
exists, the modal pre-fills the form fields for editing an existing note. - Modal Closing: Clicking outside the modal or pressing the Escape key closes the modal.
<input
value={noteInput.title}
onChange={(e) => setNoteInput({ ...noteInput, title: e.target.value })}
/>
<select
value={noteInput.category}
onChange={(e) => setNoteInput({ ...noteInput, category: e.target.value })}
>
<option hidden value="">Select Category</option>
<option value="Personal">Personal</option>
<option value="Home">Home</option>
<option value="Business">Business</option>
</select>
<textarea
value={noteInput.desc}
onChange={(e) => {
setNoteInput({ ...noteInput, desc: e.target.value });
setDescCount(e.target.value.length);
}}
/>
<button onClick={handleAddNote}>{editingId ? "Edit" : "Add"}</button>
- handleAddNote: Validates the input and either adds a new note or edits an existing note.
4. Home.jsx β Notes Tabs & Filters
Home Component: Handles category tabs, the completed filter, and passes filtered notes to NotesList
.
const [activeTab, setActiveTab] = useState("ALL");
const [filterQuery, setFilterQuery] = useState("");
const [isChecked, setIsChecked] = useState(false);
const tabs = ["ALL", "PERSONAL", "HOME", "BUSINESS"];
- Clicking a tab sets
filterQuery
to filter notes by category. - The
isChecked
checkbox filters only completed notes.
<NotesList
filterQuery={filterQuery}
isChecked={isChecked}
searchQuery={searchQuery}
/>
5. NotesList.jsx β Filtering & Searching
NotesList
handles all filtering and searching logic in one place.
const filteredNotesByCategory = notes.filter(note =>
note.category.toLowerCase().includes(filterQuery.toLowerCase())
);
const filteredNotesByCompleted = isChecked
? filteredNotesByCategory.filter(note => note.completed)
: filteredNotesByCategory;
const searchedNotes = searchQuery.trim()
? filteredNotesByCompleted.filter(note =>
note.title.toLowerCase().includes(searchQuery.trim().toLowerCase())
)
: filteredNotesByCompleted;
- Notes are filtered in this order: category β completed status β search query.
- If no notes match, the
NotesStatus
component is displayed.
return (
<div className="mt-10 flex flex-wrap gap-6">
{searchedNotes.map(note => (
<NotesCard key={note.id} {...note} />
))}
</div>
);
6. NotesCard.jsx β Single Note Functionality
Each note card displays the note, allows completion toggling, editing, and deletion.
<input
type="checkbox"
checked={completed}
onChange={() => toggleNote(id)}
/>
<i onClick={() => handleNoteEdit(id)} className="fa-solid fa-pen"></i>
<i onClick={() => deleteNote(id)} className="fa-solid fa-trash"></i>
toggleNote(id)
flips the completed status of a note.handleNoteEdit(id)
opens the modal with the note's data pre-filled for editing.deleteNote(id)
removes the note from the store.
7. Zustand Stores
- useNotesStore: Manages all notes, including adding, editing, toggling completion, and persisting data to
localStorage
.
Explanation: This store handles all CRUD operations for notes and ensures that notes persist across page reloads usingexport const useNotesStore = create( persist( (set) => ({ notes: [], addNote: (noteObj) => { set((state) => ({ notes: [ ...state.notes, { ...noteObj, id: crypto.randomUUID(), createdAt: getFormattedDate(), completed: false, }, ], })); }, deleteNote: (id) => { set((state) => ({ notes: state.notes.filter((note) => note.id !== id), })); }, toggleNote: (id) => { set((state) => ({ notes: state.notes.map((note) => note.id === id ? { ...note, completed: !note.completed } : note, ), })); }, editNote: (noteObj, id) => { set((state) => ({ notes: state.notes.map((note) => note.id === id ? { ...note, ...noteObj } : note, ), })); }, }), { name: "allNotes", storage: createJSONStorage(() => localStorage), }, ), );
localStorage
. Each note includes an auto-generatedid
, a creation date, and acompleted
status.
- useModalStore: Simple store for managing the opening and closing of the modal.
Explanation: This store provides a global state to control the visibility of the modal, allowing any component to open or close it without prop drilling.export const useModalStore = create((set) => ({ isModalOpen: false, openModal: () => { set({ isModalOpen: true }); }, closeModal: () => { set({ isModalOpen: false }); }, }));
- useNoteFormStore: Manages the current note input fields and the editing ID.
Explanation: This store keeps track of the current input values for the note form and the ID of the note being edited. It simplifies editing functionality and ensures that form state is easily accessible across components.export const useNoteFormStore = create((set) => ({ noteInput: { title: "", category: "", desc: "" }, editingId: "", setNoteInput: (input) => set((state) => ({ ...state, noteInput: input })), resetNoteInput: () => set((state) => ({ ...state, noteInput: { title: "", category: "", desc: "" }, })), setEditingId: (id) => set((state) => ({ ...state, editingId: id })), }));
8. Custom Hooks
- useEscapeClose: Closes the modal when the Escape key is pressed.
Explanation: This hook improves UX by allowing users to close the modal via the Escape key and auto-focuses the title input when the modal opens, making form entry faster.export function useEscapeClose(isOpen, onClose, titleInputRef) { useEffect(() => { if (!isOpen) return; if (titleInputRef.current) { titleInputRef.current.focus(); } const handleKeyDown = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, onClose, titleInputRef]); }
useNoteForm: Handles validation, adding/editing notes, resetting the form, and managing errors.
export const useNoteForm = () => { const [errors, setErrors] = useState({}); const titleInputRef = useRef(null); const id = useId(); const [descCount, setDescCount] = useState(0); const { addNote, editNote } = useNotesStore( useShallow((state) => ({ addNote: state.addNote, editNote: state.editNote, })), ); const closeModal = useModalStore((state) => state.closeModal); const { noteInput, resetNoteInput, editingId, setEditingId } = useNoteFormStore( useShallow((state) => ({ noteInput: state.noteInput, resetNoteInput: state.resetNoteInput, editingId: state.editingId, setEditingId: state.setEditingId, })), ); const validationConfig = { title: [ { required: true, message: "Please enter a tilte" }, { minLength: 2, message: "Title should be at least 2 characters long" }, ], category: [{ required: true, message: "Please select a category" }], desc: [{ required: false, message: "" }], }; const validate = (formData) => { const errorsData = {}; Object.entries(formData).forEach(([label, value]) => { validationConfig[label].some((rule) => { if (rule.required && !value) { errorsData[label] = rule.message; return true; } if (rule.minLength && value.length < rule.minLength) { errorsData[label] = rule.message; return true; } }); }); setErrors(errorsData); return errorsData; }; const handleAddNote = () => { const validateResult = validate(noteInput); if (Object.keys(validateResult).length) { return; } if (editingId) { editNote(noteInput, editingId); resetForm(); return; } addNote(noteInput); resetForm(); }; const resetFields = () => { resetNoteInput(); setDescCount(0); setErrors({}); }; const resetForm = () => { resetFields(); setEditingId(""); closeModal(); }; return { id, titleInputRef, errors, descCount, setDescCount, handleAddNote, resetFields, resetForm, }; };
Explanation: This hook centralizes all logic for creating or editing a note. It validates inputs, handles adding or updating notes in the global store, manages character count for the description, and resets the form when cancelled or submitted. It ensures consistent behavior and reduces duplication across components.
9. Utility Function
This function provides a consistent date format for notes. It returns the current date in DD.MM.YYYY
format, which is used when creating new notes to display their creation date.
export const getFormattedDate = () => {
return new Date().toLocaleDateString("en-GB").replace(/\//g, ".");
};
Final Thoughts:
- Building this Notes App was a valuable learning experience. Through this project, I gained a deeper understanding of React functional components and how to structure a component-driven architecture effectively.
- I learned how to manage global state with Zustand, including handling modal visibility, form inputs, and notes data, which gave me confidence in using state management outside of Redux.
- Implementing form validation and custom hooks taught me how to centralize logic and make my code more reusable and maintainable.
- Working on features like real-time search, filtering, and keyboard interactions helped me think more about user experience and accessibility.
- I also learned how to persist data with localStorage and use createPortal for modals, which are practical skills for real-world applications.
- Overall, this project strengthened my React skills, understanding of state management, and front-end design practices.
- It gave me confidence to tackle more complex projects while keeping the code organized, reusable, and user-friendly.
2. Whatβs Next:
Iβm excited to keep growing and sharing along the way! Hereβs whatβs coming up:
- Posting new blog updates every 3 days to share what Iβm learning and building.
- Diving deeper into Data Structures & Algorithms with Java β check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
- Sharing regular progress and insights on X (Twitter) β feel free to follow me there and join the conversation!
Thanks for being part of this journey!
3. Conclusion:
Building this Notes App was a highly rewarding experience that allowed me to strengthen my React skills and explore practical state management with Zustand. Through this project, I learned how to:
- Manage complex state globally without prop drilling.
- Create reusable and maintainable components with functional React patterns.
- Implement form validation and error handling in a modal.
- Build dynamic filtering and search functionality for real-time user interaction.
- Persist data in localStorage to maintain state across sessions.
- Enhance user experience with keyboard interactions, focus management, and responsive UI.
Overall, this project not only reinforced my understanding of React hooks, Zustand, and component-driven architecture but also gave me confidence to tackle more advanced projects. It demonstrates how combining state management, hooks, and thoughtful UX design can lead to a polished and fully functional application.
π» Explore the full source code on GitHub: Notes App GitHub Repository
Thanks for reading! Feel free to connect or follow along as I continue building and learning in React.
Subscribe to my newsletter
Read articles from Ritik Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ritik Kumar
Ritik Kumar
π¨βπ» Aspiring Software Developer | MERN Stack Developer.π Documenting my journey in Full-Stack Development & DSA with Java.π Focused on writing clean code, building real-world projects, and continuous learning.