Module 9: Building the Complete Todo App

Time to put it all together! We'll combine our knowledge of forms, state, and lists to build a fully interactive Todo application.

1. The Goal: A Single, Cohesive App 🎯

Instead of looking at forms in isolation, let's build the complete app. Our main `TodoApp` component will be responsible for:

  • Holding the list of todos in its state.
  • Holding the current value of the input field in its state.
  • Containing the logic for adding, deleting, and toggling todos.
  • Rendering both the form and the list of todos.

2. The Complete `TodoApp` Component

Here is the full code for our application. We'll break down the new parts in the next section.


const { useState } = React;

function TodoApp() {
  // State for the list of todos
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: true },
    { id: 2, text: 'Build a project', completed: false },
  ]);

  // State for the input field
  const [inputValue, setInputValue] = useState("");

  // Handler for adding a new todo
  const handleAddTodo = (event) => {
    event.preventDefault();
    if (inputValue.trim() === "") return;

    const newTodo = {
      id: Date.now(), // Simple unique ID
      text: inputValue,
      completed: false,
    };

    setTodos([...todos, newTodo]);
    setInputValue(""); // Clear input after adding
  };

  // Handler for deleting a todo
  const handleDeleteTodo = (idToDelete) => {
    setTodos(todos.filter(todo => todo.id !== idToDelete));
  };

  // Handler for toggling a todo's completed status
  const handleToggleComplete = (idToToggle) => {
    setTodos(
      todos.map(todo => 
        todo.id === idToToggle ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  return (
    <div class="card p-3">
      <h3 class="text-center">My Todo List</h3>
      
      {/* The Form for adding todos */}
      <form onSubmit={handleAddTodo} className="d-flex mb-3">
        <input
          type="text"
          className="form-control"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Add a new task..."
        />
        <button type="submit" className="btn btn-primary ms-2">Add</button>
      </form>

      {/* The List of todos */}
      <div className="list-group">
        {todos.map(todo => (
          <div key={todo.id} className={`list-group-item todo-item ${todo.completed ? 'completed' : ''}`}>
            <span onClick={() => handleToggleComplete(todo.id)}>
              {todo.text}
            </span>
            <button onClick={() => handleDeleteTodo(todo.id)} className="btn btn-sm btn-danger">
              ×
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<TodoApp />);

3. How the "Add Todo" Logic Works

The `handleAddTodo` function is the key piece that connects our form to our list state:

  1. The user types in the input, and the `inputValue` state is updated on every keystroke (this is the "controlled component" pattern).
  2. The user clicks "Add" or hits Enter, which triggers the form's `onSubmit` event.
  3. Our `handleAddTodo` function is called.
  4. It creates a `newTodo` object using the current `inputValue` from state.
  5. It then calls `setTodos`, creating a new array containing all the old todos plus the new one.
  6. Finally, it calls `setInputValue("")` to clear the input field for the next entry.

Because the list rendering is tied to the `todos` state, as soon as we call `setTodos`, React automatically re-renders the list to include the new item. All the pieces work together!