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:
- The user types in the input, and the `inputValue` state is updated on every keystroke (this is the "controlled component" pattern).
- The user clicks "Add" or hits Enter, which triggers the form's `onSubmit` event.
- Our `handleAddTodo` function is called.
- It creates a `newTodo` object using the current `inputValue` from state.
- It then calls `setTodos`, creating a new array containing all the old todos plus the new one.
- 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!