Todo app adalah daftar tugas yang bisa kamu tambah, centang, hapus, dan saring. Ini proyek pertama yang pas, karena hampir semua yang sudah kamu pelajari di track React kepakai bareng: state, event, list dengan key, conditional rendering, dan komponen. Bayangin daftar belanja yang ditempel di kulkas. Kertasnya cuma satu: kamu menambah barang, mencoret yang sudah dibeli, menghapus yang batal, dan kadang cuma membaca yang belum dicoret. "Yang belum dicoret" bukan kertas kedua, itu cara membaca kertas yang sama.
🧪 Tebakan cepat: waktu filter "Aktif" dipilih, daftar yang tampil perlu disimpan di state sendiri, atau cukup dihitung dari
todos? Tebak dulu, jawabannya ada di bagian "Yang tampil dihitung, bukan disimpan".
Menyiapkan proyeknya
Proyek React baru biasanya dibuat dengan Vite, lewat terminal:
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm run dev
npm create vite@latestbikin foldertodo-appberisi proyek React yang siap jalan.npm installmengunduh semua library yang dibutuhkan proyek ke foldernode_modules.npm run devmenyalakan server development. Terminal menampilkan alamat sepertihttp://localhost:5173. Buka di browser, dan tiap kali kamu menyimpan file, halamannya langsung ikut berubah.
Daftar library yang dipakai proyek (namanya dependency) dan perintah-perintahnya disimpan di package.json. Ini versi pendeknya:
{
"name": "todo-app",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}
npm run dev menjalankan perintah bernama dev di bagian "scripts". Proyek lama yang dibuat dengan Create React App memakai npm start.
Rencanakan dulu: komponen, state, hierarki
Sebelum ngoding, rencanakan dalam tiga langkah. Cara ini disebut "Thinking in React":
- Pecah UI jadi komponen. Kolom ketik dan tombol Tambah jadi
TodoForm, tombol filter jadiFilterBar, tiap baris jadiTodoItem, danAppmembungkus semuanya. - Tentukan state. State adalah data yang berubah dan harus diingat. Di sini cuma tiga: daftar
todos, isi kolom ketiktext, danfilteryang dipilih. Taruh tiap state di komponen paling bawah yang masih membungkus semua komponen yang membutuhkannya. - Susun hierarki dan alur data. Data turun lewat props, perubahan naik lewat fungsi yang dikirim sebagai props.
App state: todos, filter
├─ TodoForm state: text props: onAdd
├─ FilterBar props: filter, onChange
└─ TodoItem (satu per todo) props: todo, onToggle, onDelete
text cuma dipakai TodoForm, jadi tinggal di sana. todos dipakai daftar dan penghitung, filter dipakai FilterBar dan daftar, jadi keduanya tinggal di App.
Tiga perubahan, semuanya array baru
Mulai dari dua todo, lalu tiga aksi berturut-turut:
| Aksi | todos sesudahnya |
|---|---|
| Awal | Beli beras (belum), Cuci motor (selesai) |
| Tambah "Belajar React" | Beli beras (belum), Cuci motor (selesai), Belajar React (belum) |
| Centang "Beli beras" | Beli beras (selesai), Cuci motor (selesai), Belajar React (belum) |
| Hapus "Cuci motor" | Beli beras (selesai), Belajar React (belum) |
import { useState } from "react"
const FILTERS = { all: "Semua", active: "Aktif", done: "Selesai" }
function TodoForm({ onAdd }) {
const [text, setText] = useState("")
const add = () => {
if (!text.trim()) return // abaikan isian kosong
onAdd(text.trim())
setText("") // kosongkan kolom ketik
}
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={add}>Tambah</button>
</div>
)
}
function FilterBar({ filter, onChange }) {
return (
<div>
{Object.entries(FILTERS).map(([value, label]) => (
<button key={value} disabled={value === filter} onClick={() => onChange(value)}>
{label}
</button>
))}
</div>
)
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.text}
<button onClick={() => onDelete(todo.id)}>Hapus</button>
</li>
)
}
export default function App() {
const [todos, setTodos] = useState([
{ id: 1, text: "Beli beras", done: false },
{ id: 2, text: "Cuci motor", done: true },
])
const [filter, setFilter] = useState("all")
const addTodo = (text) => setTodos([...todos, { id: Date.now(), text, done: false }])
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 visible = todos.filter((t) => (filter === "active" ? !t.done : filter === "done" ? t.done : true))
const left = todos.filter((t) => !t.done).length
return (
<div>
<TodoForm onAdd={addTodo} />
<FilterBar filter={filter} onChange={setFilter} />
<ul>
{visible.map((t) => (
<TodoItem key={t.id} todo={t} onToggle={toggleTodo} onDelete={deleteTodo} />
))}
</ul>
<p>{left} belum selesai</p>
</div>
)
}
[...todos, { ... }]bikin array baru: semua isi lama, ditambah satu todo di belakang.todos.map(...)bikin array baru yang jumlahnya sama. Todo yangid-nya cocok diganti object baru{ ...t, done: !t.done }, sisanya dipakai apa adanya.todos.filter(...)bikin array baru tanpa todo yangid-nya sama.- Kenapa harus baru: React membandingkan state lama dengan yang baru. Kalau kamu
todos.push(...)lalusetTodos(todos), array-nya masih yang sama, jadi React menganggap gak ada yang berubah dan gak render ulang. key={t.id}membuat tiap baris dikenali dariid-nya, bukan dari urutannya. Waktu satu baris dihapus atau disaring, centangnya gak pindah ke baris lain.
Yang tampil dihitung, bukan disimpan
visible dan left gak disimpan di state. Keduanya dihitung ulang dari todos dan filter setiap kali App render. Itu jawaban tebakannya: cukup dihitung. Kalau daftar yang tampil kamu simpan sebagai state kedua, tiap tambah, centang, dan hapus harus meng-update dua tempat. Lupa sekali, dua daftarnya gak sinkron lagi.
Dengan data tadi, sesudah menambah "Belajar React" dan mencentang "Beli beras": todos berisi 3 todo, filter Aktif menampilkan 1 (Belajar React), dan left bernilai 1. Dua todo yang selesai gak hilang, cuma gak ditampilkan.
⚠️ Jebakan yang sering ketemu
- Mengubah array atau object langsung.
todos.push(baru)atautodo.done = true, lalu set state: isinya berubah, tapi layar gak. Selalu bikin yang baru dengan spread,map, ataufilter. - Index sebagai
key.key={i}bikin centang bisa nempel ke baris yang salah waktu ada yang dihapus atau disaring. Pakaiid. - Lupa mengosongkan kolom ketik. Sesudah menambah, panggil
setText(""). Latihannya mengecek ini: sesudah tombol ditekan, kolom ketiknya harus kosong lagi. - Isian kosong ikut masuk. Cek
text.trim()dulu, supaya spasi doang gak jadi todo.
TL;DR: Bikin proyek dengan npm create vite@latest, jalankan dengan npm run dev. Dependency dan script-nya tercatat di package.json. Rencanakan dalam tiga langkah: pecah UI jadi komponen, tentukan state dan di mana tinggalnya, lalu susun hierarkinya (data turun lewat props, perubahan naik lewat fungsi). Todo app cuma butuh tiga state: todos, text, filter. Tambah dengan [...todos, baru], centang dengan map, hapus dengan filter, dan hitung daftar yang tampil dari state setiap render, jangan disimpan dua kali.