Proyek: Todo App — React

Saatnya gabungin semua yang kamu pelajarin jadi proyek nyata. Todo App adalah "Hello World" tingkat lanjut di dunia React — sederhana di permukaan tapi cukup…

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

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":

  1. Pecah UI jadi komponen. Kolom ketik dan tombol Tambah jadi TodoForm, tombol filter jadi FilterBar, tiap baris jadi TodoItem, dan App membungkus semuanya.
  2. Tentukan state. State adalah data yang berubah dan harus diingat. Di sini cuma tiga: daftar todos, isi kolom ketik text, dan filter yang dipilih. Taruh tiap state di komponen paling bawah yang masih membungkus semua komponen yang membutuhkannya.
  3. 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>
  )
}

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

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.

Yang akan kamu pelajari