DAY 31: Mastered useMemo & useCallback Hook β€” Build a Performant Todo App | My Web Dev Journey – ReactJS

Ritik KumarRitik Kumar
13 min read

πŸš€ Introduction

Welcome to Day 31 of my Web Development Journey!
After laying down a strong foundation with HTML, CSS, and JavaScript, I’ve been exploring the power of ReactJS β€” a library that's become essential for building dynamic, interactive user interfaces.

Over the past few days, I’ve focused on mastering key performance optimization hooks in React, specifically useMemo and useCallback, and applied them in a real-world project β€” a fully functional Todo List App.

To stay consistent and share what I learn, I’m documenting this journey publicly. Whether you're learning React from scratch or just want a refresher, I hope this blog gives you something valuable!

πŸ“… Here’s What I Covered Over the Last 3 Days:

Day 28:

  • Deep-dived into the useMemo Hook to optimize expensive calculations
  • Explored useCallback for preventing unnecessary re-renders of child components

Day 29:

  • Started building a Todo List App
  • Implemented task addition, validation, and dynamic UI updates

Day 30:

  • Added task deletion and editing features
  • Used React Context API for global task state
  • Persisted tasks with localStorage
  • Created a custom useLocalStorage Hook for sync between state and storage

Let’s explore these concepts and the app in more detail below πŸ‘‡


1. useMemo Hook:

The useMemo Hook is a performance optimization tool in React. It helps us to memoize the result of a computation β€” meaning it caches the result β€” so that React doesn’t recalculate it on every render unless its dependencies change.

This is particularly useful for expensive calculations or pure functions that return the same output for the same input.

Syntax:

const memoizedValue = useMemo(() => {
  // Computation logic here
  return value;
}, [dependencies]);
  • () => {}: A function that returns the computed value.
  • [dependencies]: An array of dependencies that the memoized result depends on. React recalculates the value only if one of the dependencies changes.

How It Works?

When our component renders:

  • If the dependencies haven’t changed since the last render, React reuses the previously memoized value.
  • If any dependency has changed, the function is re-evaluated, and the new result is memoized.

This avoids unnecessary recalculations, making our app more efficient.

Example:

import React, { useState, useMemo } from "react";

const ExpensiveComponent = ({ num }) => {
  const expensiveCalculation = (number) => {
    console.log("Calculating...");
    let total = 0;
    for (let i = 0; i < 1e7; i++) {
      total += number;
    }
    return total;
  };

  const memoizedResult = useMemo(() => expensiveCalculation(num), [num]);

  return (
    <div>
      <p>Result: {memoizedResult}</p>
    </div>
  );
};

In this example:

  • expensiveCalculation simulates a CPU-heavy function.
  • The result is memoized, so it only recalculates when num changes.

Real-World Use Cases:

  • Expensive calculations: When rendering requires heavy computations (e.g., filtering a large list).
  • Optimizing child components: When we pass computed props to memoized child components.
  • Data transformation: When transforming fetched data (e.g., sorting, mapping) before rendering.

When Not to Use useMemo?

  • Premature optimization: Don’t use it just because we can β€” use it only when there's a measurable performance benefit.
  • Simple values/functions: If the computation is cheap, memoization adds unnecessary complexity.
  • Frequent re-renders: If the dependencies change often, memoization won’t help and might even hurt performance.

Final Thoughts:

  • useMemo is a valuable tool for optimizing performance in React apps.
  • It helps prevent unnecessary recalculations of expensive or complex computations.
  • Use it only when needed β€” avoid premature optimization.
  • Always measure performance before and after applying it.
  • If there’s no noticeable lag or bottleneck, it’s often best to skip using useMemo.
  • Keep our code readable and maintainable; don’t sacrifice clarity for micro-optimizations.

2. useCallback Hook:

The useCallback Hook is used to memoize functions in React.
It returns a memoized version of a callback function, which is only re-created when one of its dependencies changes.

This is especially useful when passing functions to child components that rely on reference equality to prevent unnecessary re-renders.

Syntax:

const memoizedCallback = useCallback(() => {
  // our callback logic here
}, [dependencies]);
  • () => {}: The callback function you want to memoize.
  • [dependencies]: An array of dependencies. The callback is recreated only if one of these changes.

How It Works?

When your component renders:

  • If the dependencies haven’t changed, React returns the previously memoized function.
  • If any dependency has changed, the function is re-created and returned.

This helps optimize performance by avoiding unnecessary re-renders in child components receiving functions as props.

Example:

import React, { useState, useCallback } from "react";

const Button = React.memo(({ handleClick, label }) => {
  console.log(`Rendering ${label}`);
  return <button onClick={handleClick}>{label}</button>;
});

const Counter = () => {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(false);

  const increment = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Button handleClick={increment} label="Increment" />
      <button onClick={() => setOtherState(!otherState)}>
        Toggle Other State
      </button>
    </div>
  );
};

In this example:

  • The increment function is memoized using useCallback.
  • The Button component is wrapped in React.memo(), so it only re-renders if its props change.
  • Without useCallback, increment would be a new function on every render, causing unnecessary re-renders.

Real-World Use Cases:

  • Prevent unnecessary child re-renders: When passing functions to memoized components (React.memo).
  • Stable event handlers: When event handlers don’t need to change across renders.
  • Memoized dependencies: When a function is a dependency of useEffect, useMemo, or other hooks.

When Not to Use useCallback

  • No performance issue: If our component isn’t facing re-render issues, don’t use it unnecessarily.
  • Function doesn’t change often: If the function rarely gets recreated, useCallback won’t offer much benefit.
  • Complex dependency arrays: Overuse can make our code harder to read and maintain.

Final Thoughts:

  • useCallback is great for function memoization, especially when working with memoized child components.
  • Use it when we want to ensure function identity is preserved between renders.
  • Don’t use it blindly β€” always check if there's an actual performance gain.
  • Keep our code clean, and optimize only when necessary.

3. Todo List App:

After practicing React hooks like useMemo and managing global state with Context and custom hooks, I wanted to build a practical productivity tool. This inspired me to create a Todo List App β€” a straightforward yet feature-rich app to add, edit, filter, and manage tasks efficiently.

Project Overview:

This Todo List App enables users to:

  • Add new tasks with validation to avoid empty or duplicate entries
  • Edit existing tasks seamlessly with input focus management
  • Mark tasks as completed or pending with a simple click
  • Filter tasks by status: all, pending, or completed
  • Clear all completed tasks with one action
  • Persist tasks locally using localStorage to keep data across sessions
  • Display real-time counts of pending tasks
  • Enjoy a clean, intuitive UI with responsive controls

The app is built entirely with React functional components and hooks, leveraging Context API for global state management and custom hooks for local storage integration.

Core Features:

Task Management

  • Add Tasks: Users can add new tasks, with input validation preventing empty or duplicate task titles.
  • Edit Tasks: Edit mode lets users update task titles; input field auto-focuses for smooth editing.
  • Toggle Completion: Click on the checkbox to mark tasks as completed or revert to pending.
  • Delete Tasks: Remove individual tasks easily via delete icons.
  • Filter Tasks: Switch between viewing all, pending, or completed tasks with filter buttons.
  • Clear Completed: Clear all completed tasks in one click, helping keep the list tidy.

State & Validation

  • Error Handling: Displays error messages for invalid input (empty or duplicate tasks).
  • Real-time Filtering: Tasks list updates instantly when the filter or tasks change, optimized with useMemo.
  • Task Counting: Shows the count of pending tasks dynamically.

Data Persistence

  • Local Storage Integration: Tasks are stored in localStorage, ensuring persistence across browser reloads or closures.
  • Custom Hook: useLocalStorage abstracts the sync logic between React state and local storage.

User Interface

  • Focused Inputs: The input field automatically focuses when editing a task.
  • Accessible Controls: Keyboard accessibility with Enter key to submit tasks.
  • Visual Feedback: Strikethrough style on completed tasks and checkmark icons.
  • Clear Visual Filters: Active filter button highlights to improve usability.

React Concepts Applied:

  • useState β€” for managing input value, editing state, filter selection, and error messages.
  • useContext & createContext β€” global state management for task list via TodosContext.
  • useMemo β€” memoizes filtered task lists to avoid unnecessary recalculations on re-render.
  • useEffect & useRef β€” auto-focusing input field on edit mode.
  • Custom Hook: useLocalStorage for syncing state with local storage.
  • Component composition β€” breaking UI into reusable components like AddTaskContainer, TaskList, TaskItem, TodoStatus, and buttons.
  • Props and event handling β€” passing handlers and data between components for interaction.
  • Conditional rendering and dynamic class names β€” for visual task states and active filters.

Folder Structure:

src/
β”œβ”€β”€ App.jsx
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ AddTaskContainer.jsx
β”‚   β”œβ”€β”€ Button.jsx
β”‚   β”œβ”€β”€ StatusButtons.jsx
β”‚   β”œβ”€β”€ TaskItem.jsx
β”‚   β”œβ”€β”€ TaskList.jsx
β”‚   └── TodoStatus.jsx
β”œβ”€β”€ contexts/
β”‚   β”œβ”€β”€ TodosContext.jsx
β”‚   └── useTodosContext.js
β”œβ”€β”€ hooks/
β”‚   └── useLocalStorage.js
β”œβ”€β”€ utils/
β”‚   β”œβ”€β”€ countIncompletedTasks.js
β”‚   └── initLocalStorage.js

Code Walkthrough:

1. Application Root & Context Setup

App.jsx is the main component where global task state is accessed via useTodosContext.
The filteredTasks array is memoized using useMemo to improve performance by recalculating only when the tasks or filter change.

const App = () => {
  const { tasks, setTasks } = useTodosContext();
  const [taskInput, setTaskInput] = useState("");
  const [filter, setFilter] = useState("all");
  const [editingId, setEditingId] = useState(null);
  const [error, setError] = useState("");

  const filteredTasks = useMemo(() => {
    if (filter === "pending") {
      return tasks.filter((task) => !task.completed);
    }
    if (filter === "completed") {
      return tasks.filter((task) => task.completed);
    }
    return tasks;
  }, [filter, tasks]);

  // Add, edit, validation logic here 
};

The TodosProvider in TodosContext.jsx wraps the app and provides task state and updater functions globally, using the custom useLocalStorage hook for persistence.

export const TodosProvider = ({ children }) => {
  const [tasks, setTasks] = useLocalStorage("allTodos", []);

  return (
    <TodosContext.Provider value={{ tasks, setTasks }}>
      {children}
    </TodosContext.Provider>
  );
};

2. Add, Edit and Validation Logic:

The handleAddTask function handles three core actions:

  • Validation for Empty Input: If the user tries to submit an empty task, an error message is set and the function exits.
  • Edit Mode: If editingId is present, it updates the title of the matching task and resets the form.
  • Duplicate Check: Prevents adding a task with a duplicate title (case-insensitive).
  • Task Creation: If all checks pass, a new task is created with a unique id, and added to the task list.

The handleChange function updates the taskInput state and clears any existing error when the user types in the input field.

const handleAddTask = () => {
    if (!taskInput.trim()) {
      setError("Task Cannot be Empty");
      return;
    }

    if (editingId) {
      setTasks((prevTasks) => {
        return prevTasks.map((task) => {
          return task.id === editingId ? { ...task, title: taskInput } : task;
        });
      });
      setEditingId(null);
      setTaskInput("");
      setError("");
      return;
    }

    const isDuplicate = tasks.some(
      (task) => task.title.toLowerCase() === taskInput.trim().toLowerCase()
    );
    if (isDuplicate) {
      setError("Task already exists");
      return;
    }

    const task = {
      id: crypto.randomUUID(),
      title: taskInput.trim(),
      completed: false,
    };

    setTasks((prevTask) => [...prevTask, task]);
    setTaskInput("");
    setError("");
  };

  const handleChange = (e) => {
    setTaskInput(e.target.value);
    if (error) setError("");
  };

3. Adding & Editing Tasks:

AddTaskContainer manages the input field and add/save button. It handles error display and auto-focuses the input when editing a task.

const AddTaskContainer = ({ value, onClick, editingId, error, onChange }) => {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, [editingId]);

  return (
    <>
      {error && <p className="error">{error}</p>}
      <div className="add-task-container">
        <input
          type="text"
          ref={inputRef}
          placeholder="Add Task..."
          value={value}
          onChange={onChange}
          onKeyDown={(e) => e.key === "Enter" && onClick()}
        />
        <button onClick={onClick}>
          {editingId ? "Save Task" : "Add Task"}
        </button>
      </div>
    </>
  );
};

4. Task List & Task Items:

TaskList renders tasks filtered by status. Each TaskItem handles toggling completion, editing, and deletion.

const TaskList = ({ filteredTasks, setTaskInput, setEditingId }) => {
  const handleEdit = (id) => {
    const { title } = filteredTasks.find((task) => task.id === id);
    setTaskInput(title);
    setEditingId(id);
  };

  return (
    <ul className="task-list">
      {filteredTasks.map((task) => {
        return <TaskItem key={task.id} task={task} handleEdit={handleEdit} />;
      })}
    </ul>
  );
};
const TaskItem = ({ task, handleEdit }) => {
  const { setTasks } = useTodosContext();
  const { id, title, completed } = task;

  const handleCheckbox = () => {
    setTasks((prev) =>
      prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    );
  };

  const handleDelete = () => {
    setTasks((prev) => prev.filter((t) => t.id !== id));
  };

  return (
    <li>
      <div onClick={handleCheckbox} className={`checkbox ${completed ? "completed" : ""}`}>
        {completed && <i className="fa-solid fa-check"></i>}
      </div>
      <div className={`task ${completed ? "strike" : ""}`}>{title}</div>
      <i onClick={() => handleEdit(id)} className="fa-regular fa-pen-to-square"></i>
      <i onClick={handleDelete} className="fa-solid fa-trash"></i>
    </li>
  );
};

5. Filtering & Status:

TodoStatus displays task counts, filter buttons, and a clear-completed action.

const TodoStatus = ({ filteredTasks, setFilter, filter }) => {
  const { setTasks } = useTodosContext();
  const count = countIncompletedTasks(filteredTasks);

  const handleClearCompleted = () => {
    setFilter("all");
    setTasks((prev) => prev.filter((task) => !task.completed));
  };

  return (
    <div className="status-container">
      <p>{count} {count > 1 ? "tasks" : "task"} left</p>
      <StatusButtons filter={filter} setFilter={setFilter} />
      <Button onClick={handleClearCompleted}>Clear Completed</Button>
    </div>
  );
};
const StatusButtons = ({ setFilter, filter }) => {
  return (
    <div className="btn-container">
      <Button
        onClick={() => setFilter("all")}
        btnClass="all-btn"
        activeClass={filter === "all" ? "active" : ""}
        label="All"
      />
      <Button
        onClick={() => setFilter("pending")}
        btnClass="pending-btn"
        label="Pending"
        activeClass={filter === "pending" ? "active" : ""}
      />
      <Button
        onClick={() => setFilter("completed")}
        btnClass="completed-btn"
        label="Completed"
        activeClass={filter === "completed" ? "active" : ""}
      />
    </div>
  );
};

6. Custom Hook: useLocalStorage:

Manages syncing state with localStorage for persistent task storage.

export const useLocalStorage = (key, initialData) => {
  const [data, setData] = useState(() => initLocalStorage(key, initialData));

  const updateLocalStorage = (newData) => {
    const valueToStore = typeof newData === "function" ? newData(data) : newData;
    localStorage.setItem(key, JSON.stringify(valueToStore));
    setData(valueToStore);
  };

  return [data, updateLocalStorage];
};

Final Thoughts:

This Todo List project was a great exercise in applying React fundamentals like useState, useContext, useMemo, and custom hooks in a practical setting.

  • Handling input validation, task editing, and real-time filtering helped deepen my understanding of state and event management.
  • Using Context API paired with a custom useLocalStorage hook made state sharing and persistence simple and clean.
  • The modular component design and clear separation of concerns made the codebase scalable and maintainable.

Overall, this app reinforced best practices for managing React state and building performant, user-friendly interfaces.


4. 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!


5. Conclusion:

In this blog of my web dev journey, I explored two essential React hooks β€” useMemo and useCallback β€” and how they help optimize performance and manage component re-renders effectively.

  • I broke down the purpose and behavior of each hook, showing when and why to use them in real-world React apps.
  • I built a fully functional Todo List App to put these hooks into practice, along with other React fundamentals like state management, context API, custom hooks, and conditional rendering.
  • I implemented task filtering, editing, validation, and localStorage persistence, ensuring the app remains responsive, clean, and user-friendly.
  • The modular structure and use of React best practices made the codebase easy to scale and maintain.

This project helped solidify my understanding of optimizing React performance with memoization and structuring clean, reusable components. If you're learning React, I highly recommend getting hands-on with these hooks β€” they’re game-changers once you know when to use them.

Thanks for reading, and feel free to connect or follow along as I continue building and learning.

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.