Build a Todo App
A complete todo application with React and Next.js
What you'll learn
Plan the component tree before writing code
Manage application state with React hooks
Implement CRUD operations for todo items
Add filtering and sorting for list display
Persist state to localStorage for data retention
Style a complete UI with Tailwind CSS
Build a functional todo application that demonstrates core React concepts: state management, component composition, and user interactions.
Requirements
- Add new todos with a text input
- Mark todos as complete (toggle)
- Delete individual todos
- Filter todos: All / Active / Completed
- Show total and completed count
Starter Code
"use client"
import { useState } from "react"
interface Todo {
id: string
text: string
completed: boolean
}
export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([])
const [input, setInput] = useState("")
// TODO: Implement addTodo
// TODO: Implement toggleTodo
// TODO: Implement deleteTodo
return (
<div className="mx-auto max-w-md p-4">
<h1 className="mb-4 text-2xl font-bold">Todo App</h1>
{/* Add todo form */}
<div className="mb-4 flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a todo..."
className="flex-1 rounded border px-3 py-2"
/>
<button className="rounded bg-blue-500 px-4 py-2 text-white">
Add
</button>
</div>
{/* Todo list */}
<ul className="space-y-2">
{todos.map((todo) => (
<li key={todo.id} className="flex items-center gap-2">
{/* TODO: toggle button */}
{/* TODO: todo text */}
{/* TODO: delete button */}
</li>
))}
</ul>
</div>
)
}
Hints
Need a hint?
Use crypto.randomUUID() or Date.now().toString() to generate unique IDs for new todos.
Toggle logic
const toggleTodo = (id: string) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
)
}
Filter approach
const [filter, setFilter] = useState<"all" | "active" | "completed">("all")
const filtered = todos.filter((todo) => {
if (filter === "active") return !todo.completed
if (filter === "completed") return todo.completed
return true
})
Next Steps
- Expense Tracker — Track your spending
- Dashboard — Build a data dashboard
Key Takeaways
Planning the component tree before coding prevents structural refactors mid-implementation
useState manages the todo list; useReducer scales better for complex state transitions
CRUD operations map directly to user actions: create (add), read (list), update (toggle/edit), delete (remove)
Filtering and sorting are derived state — computed from the source array, not stored separately
localStorage persists state across sessions; serialize with JSON.stringify and parse on load