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(“”);
};
type=”text”
placeholder=”Add task…”
value={task}
onChange={(e) => setTask(e.target.value)}
className=”border rounded px-2 py-2 w-80 m-2″
/>
onClick={handleSubmit}
className=”border rounded p-2 w-25 cursor-pointer hover:bg-gray-400 hover:text-black”
Add Task
{todos.map((todo, 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;
}
This helps maintain clean and meaningful data.
- Immutable State Updates (Arrays)
Used the spread operator to update the todos array:
setTodos([…todos, task]);
- …todos copies all existing tasks
- task adds the new task at the end
- A new array is created instead of modifying the old one
- This follows React’s immutability rule and ensures proper re-rendering.
- 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) => (
))}