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} />
))}
Learnings:
- Controlled Inputs Learned how to manage form inputs using useState, keeping the input value fully controlled by React state.
-
Basic Validation
Added validation to prevent empty or whitespace-only todos from being added:if (task.trim() === ""){
alert("Enter task to add");
return;
} Immutable State Updates (Arrays)
Used the spread operator to update the todos array:
jsx
setTodos([...todos, task]);
Resetting Input for Better UX
Cleared the input field after adding a todo:
setTask("");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>
Learnings:
- Deleting a Todo the Right Way: Used filter() to remove a specific todo instead of changing the original array.
- 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.
- Understanding Immutability: filter() creates a new array instead of changing the old one. This follows React’s rule of not mutating state directly.
- 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>
Learnings:
- Toggling State Using map(): Used map() to update only the clicked todo without affecting others.
- Conditional Rendering: Displayed the ✔ mark only when completed is true.
- Deleting a Todo Safely: Used filter() to remove a specific todo from state.
- Avoiding Index-Based Bugs: Used a unique id instead of array index for both toggle and delete logic.
- Immutability in React State: Both map() and filter() return new arrays, keeping state updates safe.
Top comments (0)