DEV Community

Abhishek Arankal
Abhishek Arankal

Posted on

Todo App

Introduction

After completing my first logic-focused project (Counters), I wanted to take the next natural step in complexity — not by improving the UI, but by challenging my thinking.

This led me to Project 2: Todo App (Logic-First, Not UI).

Why a Todo App?

Counters helped me understand single-value state logic.
A Todo app forces you to think in terms of:

  • Arrays instead of single values
  • CRUD operations (add, update, delete)
  • Conditional rendering
  • Edge cases and state consistency

This is where many React learners start to struggle — which makes it the perfect place to grow.

The goal of this project was not to build a fancy interface, but to strengthen my problem-solving approach, write predictable state updates, and handle real-world logic scenarios that appear in almost every frontend application.

In this article, I’ll break down how I approached the Todo app step by step, the logic decisions I made, the mistakes I encountered, and what I learned by solving them.

Level-by-Level Development 🚀

Level 1: Add Todo Creation with Validation

Commit: Level 1: Add todo creation with validation

const [todos, setTodos] = useState([]);
const [task, setTask] = useState("");

const handleSubmit = () => {
  if (task.trim() === "") {
    alert("Enter task to add");
    return;
  }
  setTodos([...todos, task]);
  setTask("");
};

<input type="text" placeholder="Add task..." value={task} 
onChange={(e) =>setTask(e.target.value)} className="border rounded px-2 py-2 w-80 m-2"/>
<button
  onClick={handleSubmit}
  className="border rounded p-2 w-25 cursor-pointer hover:bg-gray-400 hover:text-black"
>
  Add Task
</button>

{todos.map((todo, idx) => (
  <TaskCard todo={todo} key={idx} />
))}
Enter fullscreen mode Exit fullscreen mode

Learnings:

  1. Controlled Inputs Learned how to manage form inputs using useState, keeping the input value fully controlled by React state.
  2. Basic Validation
    Added validation to prevent empty or whitespace-only todos from being added:

    if (task.trim() === ""){
    alert("Enter task to add");
    return;
    }

  3. Immutable State Updates (Arrays)
    Used the spread operator to update the todos array:
    jsx
    setTodos([...todos, task]);

  4. Resetting Input for Better UX
    Cleared the input field after adding a todo:
    setTask("");

  5. Rendering Dynamic Lists
    Used map() to render todos dynamically:
    {todos.map((todo, idx) => (
    <TaskCard todo={todo} key={idx} />
    ))}

Level 2: Delete Todo (Filter Logic)
commit:Level2: Add delete todo functionality

Goal:

  • Allow users to delete a todo
  • Remove only the selected todo
  • Keep the remaining todos safe
  • Learn proper state removal logic in React

Core Idea:
Instead of modifying the existing todos array, create a new array(updatedTodos) that excludes the selected todo and update the state with it.

Logic:

const handleDeleteTask = (selectedTodo) => {
  const updatedTodos = todos.filter((todo) => todo.id !== selectedTodo.id);
setTodos(updatedTodos);
};`

<button className='px-2 py-0.5 border rounded text-sm bg-red-300 text-black' onClick={() =>handleDelete(todo)}>Delete</button>
Enter fullscreen mode Exit fullscreen mode

Learnings:

  1. Deleting a Todo the Right Way: Used filter() to remove a specific todo instead of changing the original array.
  2. Why Not Use Index: Using index can cause bugs when items are added or removed. So I deleted todos using a unique id, which is safer and more reliable.
  3. Understanding Immutability: filter() creates a new array instead of changing the old one. This follows React’s rule of not mutating state directly.
  4. Predictable State Updates: Even after deleting multiple todos, the state stays clean and behaves as expected.

Level 3: Completing a Task (State Toggle Logic in React)
commit: Level 3: Add toggle completed functionality

Goal:

  • Mark a todo as completed or not completed
  • Delete a specific todo
  • Handle state updates safely
  • Improve logic without breaking existing features

Logic:

const handleTaskCompleted = (todo) => {
  const updatedTodos = todos.map((t) => t.id === todo.id ? {...t, completed: !t.completed} : t)
  setTodos(updatedTodos)
}
<button className='w-5 h-5 border' onClick={() =>handleTaskCompleted(todo)}>{todo.completed && <span className="text-md font-bold leading-none"></span>}</button>
Enter fullscreen mode Exit fullscreen mode

Learnings:

  1. Toggling State Using map(): Used map() to update only the clicked todo without affecting others.
  2. Conditional Rendering: Displayed the ✔ mark only when completed is true.
  3. Deleting a Todo Safely: Used filter() to remove a specific todo from state.
  4. Avoiding Index-Based Bugs: Used a unique id instead of array index for both toggle and delete logic.
  5. Immutability in React State: Both map() and filter() return new arrays, keeping state updates safe.

Top comments (0)