Saatnya gabungin semua yang kamu pelajarin jadi proyek nyata. Todo App adalah "Hello World" tingkat lanjut di dunia React — sederhana di permukaan tapi cukup nyentuh banyak konsep penting: state management, event handling, list rendering dengan key, conditional rendering, immutable update, dan komposisi komponen. Tiap company di Indonesia punya derivatif Todo App — Trello, Notion, ClickUp, Asana — semua dasarnya: list of items + CRUD + filter. Bangun fondasi pattern ini = unlock kemampuan bangun aplikasi yang lebih kompleks.
💡 "Toy project" yang ngajarin banyak. Walaupun sederhana, Todo App nyentuh: useState (data + UI state), event handling (onClick/onChange/onSubmit), list rendering (map + key), immutable update (spread + filter + map), conditional rendering (filter view), komposisi (pecah ke komponen). Selesain ini = kamu udah punya skill bangun aplikasi React beneran.
Konsep yang Digabungin
- useState — daftar todo + input value + filter aktif
- Event handling — onClick (tambah/hapus/toggle), onChange (input), onSubmit (Enter key)
- List rendering —
.map()dengankeyunik (Date.now()atau UUID) - Conditional rendering — pesan empty state, filter view
- Komposisi — pecah jadi
<TodoInput>,<TodoList>,<TodoItem>,<TodoFilters> - Immutable update — spread + filter + map (jangan mutate!)
Fitur Standar
- Tambah todo baru (Enter atau klik tombol)
- Tandai selesai/belum (klik teks)
- Hapus todo (tombol ×)
- Filter view: Semua / Aktif / Selesai
- Bonus: persist ke localStorage
Implementation Lengkap
function App() {
const [todos, setTodos] = useState([])
const [input, setInput] = useState("")
const [filter, setFilter] = useState("all") // all | active | completed
const addTodo = (e) => {
e.preventDefault()
if (!input.trim()) return
setTodos([...todos, {
id: Date.now(),
text: input.trim(),
done: false,
}])
setInput("")
}
const toggleTodo = (id) => {
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
))
}
const deleteTodo = (id) => {
setTodos(todos.filter(t => t.id !== id))
}
const visibleTodos = todos.filter(t => {
if (filter === "active") return !t.done
if (filter === "completed") return t.done
return true
})
return (
<div>
<h1>Todo App</h1>
<form onSubmit={addTodo}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Tambah todo..."
/>
<button type="submit">Tambah</button>
</form>
<div>
<button onClick={() => setFilter("all")}>Semua</button>
<button onClick={() => setFilter("active")}>Aktif</button>
<button onClick={() => setFilter("completed")}>Selesai</button>
</div>
{visibleTodos.length === 0 ? (
<p>Belum ada todo.</p>
) : (
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.done ? "line-through" : "none" }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>×</button>
</li>
))}
</ul>
)}
</div>
)
}
Bonus: Pecah ke Komponen + localStorage Persist
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initial
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}
function App() {
const [todos, setTodos] = useLocalStorage("todos", [])
const [filter, setFilter] = useState("all")
return (
<div>
<TodoInput onAdd={(text) => setTodos([...todos, {id: Date.now(), text, done: false}])} />
<TodoFilters current={filter} onChange={setFilter} />
<TodoList
todos={todos.filter(t => filter === "all" ? true : filter === "active" ? !t.done : t.done)}
onToggle={(id) => setTodos(todos.map(t => t.id === id ? {...t, done: !t.done} : t))}
onDelete={(id) => setTodos(todos.filter(t => t.id !== id))}
/>
</div>
)
}
Next Step — Beyond Basic
Setelah Todo App basic jalan, coba upgrade:
- Drag-drop reorder (pakai
@dnd-kit/core) - Dueta date + reminder (notification API)
- Categories (tag, project)
- Sync across tabs (BroadcastChannel atau real-time)
- Convert ke fullstack (Next.js + database)
- Add tests (Vitest + React Testing Library)
🎭 Analogi sehari-hari
Todo App itu kayak resep nasi goreng pertama — sederhana, tapi nyentuh teknik dasar masak yang dipake di hampir semua masakan: nyalain kompor, atur api, masukin bahan urut, garam secukupnya, plating. Pas kamu udah bisa nasi goreng, kamu udah punya fondasi buat masakan yang lebih kompleks: kwetiau, mie goreng, capcay. Sama, Todo App = fondasi React yang cabang ke aplikasi lain. CRUD list pattern ini ada di mana-mana: chat messages, blog posts, e-commerce cart, kanban board. Kuasai dasarnya, sisanya tinggal modifikasi.
⚠️ Jebakan yang sering ditemui
- Mutate state —
todos.push(newTodo)salah. Pakai spread:[...todos, newTodo]. - Pakai index sebagai key — pas filter atau hapus, key bergeser, React bingung. Pakai
idunik. - Lupa
e.preventDefault()di submit — page reload, todos hilang. - Filter logic di tempat random — extract ke variable
visibleTodosatau pakai useMemo kalau perlu. - Handle Enter manual di onKeyDown — pakai
<form onSubmit>lebih semantik + a11y-friendly. - Lupa
keywarning di console — selalu kasihkey={todo.id}di list. - Handle async tanpa loading state — kalau persist ke server, tampilin spinner.
TL;DR: Todo App = proyek "Hello World" tingkat 2 yang gabungin semua konsep React dasar: useState, event handling, list rendering, conditional rendering, immutable update, komposisi. Pattern CRUD list ini transferable ke chat, posts, cart, kanban — dasar hampir semua aplikasi. Selesain ini = kamu siap bangun aplikasi React beneran. Next step: drag-drop, persistence, conversion ke fullstack, testing.