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

Ritik KumarRitik Kumar
13 min read

πŸš€ 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.
  • 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, and useNoteFormStore.
  • 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's persist 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 count
  • useEffect β€” for handling escape key press and auto-focus of input fields
  • useRef β€” for managing input focus inside modal
  • useId β€” 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 press
    • useNoteForm β€” 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 both Header and Home 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 the searchQuery state in the App component, enabling real-time note filtering.

  • Add Note Modal The Add button triggers openModal from useModalStore, toggling the modal state.
    The AddNoteModal 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.
    export 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),
      },
    ),
    );
    
    Explanation: This store handles all CRUD operations for notes and ensures that notes persist across page reloads using localStorage. Each note includes an auto-generated id, a creation date, and a completed status.
  • useModalStore: Simple store for managing the opening and closing of the modal.
    export const useModalStore = create((set) => ({
    isModalOpen: false,
    openModal: () => {
      set({ isModalOpen: true });
    },
    closeModal: () => {
      set({ isModalOpen: false });
    },
    }));
    
    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.
  • useNoteFormStore: Manages the current note input fields and the editing ID.
    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 })),
    }));
    
    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.

8. Custom Hooks

  • useEscapeClose: Closes the modal when the Escape key is pressed.
    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]);
    }
    
    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.
  • 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.

1
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.