Bab 1: Bereaksi Terhadap Input dengan State

6 menit baca

Pendahuluan: UI Itu Hidup, Bukan Patung

Coba bayangin kamu lagi pesan makanan di aplikasi ojol. Kamu ketik nama restoran, muncul daftar. Kamu klik satu menu, tombol "Tambah ke Keranjang" berubah jadi "✓ Ditambahkan". Kamu checkout, muncul loading spinner, terus muncul konfirmasi pesanan.

Semua perubahan visual itu... siapa yang ngatur? Di dunia React, jawabannya adalah state. Dan cara kita "berpikir" tentang perubahan UI ini sangat berbeda dari cara tradisional.

Di bab ini, kamu bakal belajar cara berpikir tentang UI secara deklaratif, bukan imperatif. Ini fondasi paling penting sebelum kamu bikin aplikasi React yang kompleks.


Imperatif vs Deklaratif: Dua Cara Berpikir

💡Info

Cara Imperatif (kayak naik taksi biasa):

  • "Pak, belok kiri di perempatan depan"
  • "Terus lurus sampai lampu merah"
  • "Belok kanan, masuk gang kedua"
  • "Stop di rumah cat biru"

Kamu kasih instruksi langkah demi langkah. Kalau salah satu instruksi keliru, nyasar.

Cara Deklaratif (kayak pakai GPS di ojol):

  • "Antar saya ke Jl. Merdeka No. 45"

Kamu cuma bilang tujuan akhirnya. GPS yang mikirin rutenya.

Dalam Kode

Imperatif (vanilla JavaScript):

jsx
// Kamu harus kasih tau SETIAP LANGKAH ke browser
const tombol = document.getElementById('kirim');
const form = document.getElementById('form-pesan');
const loading = document.getElementById('loading');
const sukses = document.getElementById('sukses');

tombol.addEventListener('click', function() {
  // Langkah 1: Sembunyiin form
  form.style.display = 'none';
  // Langkah 2: Tampilin loading
  loading.style.display = 'block';
  
  // Langkah 3: Setelah selesai...
  setTimeout(function() {
    // Langkah 4: Sembunyiin loading
    loading.style.display = 'none';
    // Langkah 5: Tampilin pesan sukses
    sukses.style.display = 'block';
  }, 2000);
});

Masalahnya? Kalau UI makin kompleks, kamu harus ingat semua elemen mana yang harus disembunyiin, ditampilin, diubah teksnya... Satu kelupaan, UI jadi kacau.

Deklaratif (React):

jsx
function FormPesan() {
  const [status, setStatus] = useState('mengetik'); // 'mengetik' | 'mengirim' | 'sukses'

  // Kamu cuma bilang: "Kalau statusnya X, tampilin Y"
  if (status === 'sukses') {
    return <h2>Pesan terkirim! ✅</h2>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <textarea disabled={status === 'mengirim'} />
      <button disabled={status === 'mengirim'}>
        {status === 'mengirim' ? 'Mengirim...' : 'Kirim'}
      </button>
    </form>
  );
}

Coba sendiri: Edit kode di bawah dan lihat hasilnya langsung!

Lihat bedanya? Di React, kamu gak bilang "sembunyiin ini, tampilin itu". Kamu cuma bilang: "Kalau state-nya begini, UI-nya harusnya kayak gini." React yang urus sisanya.


Berpikir dalam "Visual States"

💡Info

Lampu lalu lintas punya 3 state: 🔴 Merah, 🟡 Kuning, 🟢 Hijau.

Di setiap state, perilaku yang diharapkan JELAS:

  • Merah → mobil berhenti
  • Kuning → mobil siap-siap
  • Hijau → mobil jalan

Lampu gak pernah merah DAN hijau bersamaan. Itu "impossible state".

UI kamu juga harus dipikirin kayak gini. Setiap "layar" atau "tampilan" yang mungkin muncul adalah satu visual state.

Langkah 1: Identifikasi Semua Visual State

Misal kamu bikin form pengiriman pesan. Apa aja state yang mungkin?

jsx
// State 1: KOSONG - user belum ngetik apa-apa
// State 2: MENGETIK - user lagi ngetik pesan
// State 3: MENGIRIM - pesan lagi dikirim ke server
// State 4: SUKSES - pesan berhasil terkirim
// State 5: ERROR - ada masalah saat kirim

Visualisasiin kayak gini:

[KOSONG] → user ngetik → [MENGETIK] → user klik kirim → [MENGIRIM] ↓ berhasil? → [SUKSES] gagal? → [ERROR] → user coba lagi → [MENGIRIM]

Langkah 2: Bikin "Mockup" untuk Setiap State

Ini teknik yang super berguna. Sebelum nulis logika, bikin dulu tampilan untuk SETIAP state:

jsx
function FormPesan() {
  // Coba ganti-ganti nilai ini untuk lihat setiap state
  const status = 'kosong'; // Ganti: 'kosong', 'mengetik', 'mengirim', 'sukses', 'error'
  const error = null;      // Ganti: null, 'Gagal mengirim pesan'

  return (
    <div>
      <h2>Kirim Pesan ke CS</h2>

      {/* State SUKSES */}
      {status === 'sukses' && (
        <div className="sukses">
          <p>✅ Pesan kamu sudah terkirim!</p>
        </div>
      )}

      {/* Form (tampil di semua state KECUALI sukses) */}
      {status !== 'sukses' && (
        <form>
          <textarea
            placeholder="Tulis pesan kamu..."
            disabled={status === 'mengirim'}
          />

          {/* Pesan error */}
          {status === 'error' && (
            <p className="error">❌ {error}</p>
          )}

          <button disabled={status === 'kosong' || status === 'mengirim'}>
            {status === 'mengirim' ? '⏳ Mengirim...' : 'Kirim Pesan'}
          </button>
        </form>
      )}
    </div>
  );
}

Dengan cara ini, kamu bisa "preview" setiap state tanpa perlu logika yang rumit dulu. Ini namanya "storyboarding" UI kamu.


Menghubungkan Event Handler ke State

Oke, sekarang kita udah tau visual state-nya. Tapi gimana caranya berpindah dari satu state ke state lain?

💡Info

Mesin ATM punya state: menunggu_kartu → memasukkan_pin → memilih_transaksi → memproses → selesai

Yang bikin berpindah state? Aksi dari user:

  • Masukkin kartu → pindah ke memasukkan_pin
  • Ketik PIN benar → pindah ke memilih_transaksi
  • Pilih tarik tunai → pindah ke memproses

Di React, "aksi dari user" ini ditangkap oleh event handler.

jsx
function FormPesan() {
  const [status, setStatus] = useState('kosong');
  const [pesan, setPesan] = useState('');
  const [error, setError] = useState(null);

  // EVENT: User ngetik di textarea
  function handleKetik(e) {
    setPesan(e.target.value);
    // Kalau ada isinya, status jadi 'mengetik'
    // Kalau kosong, status balik ke 'kosong'
    setStatus(e.target.value.length > 0 ? 'mengetik' : 'kosong');
  }

  // EVENT: User klik tombol kirim
  async function handleKirim(e) {
    e.preventDefault();
    setStatus('mengirim'); // Pindah ke state mengirim

    try {
      await kirimPesan(pesan); // Panggil API
      setStatus('sukses');     // Berhasil!
    } catch (err) {
      setStatus('error');      // Gagal!
      setError(err.message);
    }
  }

  // ... render UI berdasarkan status
}

Perhatiin polanya:

  1. Event terjadi (user ngetik, klik, dll)
  2. State berubah (via setState)
  3. UI otomatis update (React re-render)

Kamu gak perlu manually ubah DOM. Cukup ubah state, React yang urus tampilannya.


Mengurangi "Impossible States"

💡Info

Bayangin formulir yang punya field "Status Pernikahan" dan "Nama Pasangan". Kalau status = "Belum Menikah", field nama pasangan harusnya gak bisa diisi. Tapi kalau kamu simpan keduanya secara terpisah tanpa aturan...

jsx
// ❌ BAHAYA: Bisa terjadi state yang mustahil
const [sedangMengirim, setSedangMengirim] = useState(false);
const [sudahTerkirim, setSudahTerkirim] = useState(false);
const [error, setError] = useState(null);

Masalahnya? Secara teknis, sedangMengirim = true DAN sudahTerkirim = true bisa terjadi bersamaan. Itu kayak lampu merah dan hijau nyala bareng. Mustahil di dunia nyata, tapi kode kamu memungkinkannya.

Solusi: Gunakan Satu State dengan Nilai yang Jelas

jsx
// ✅ AMAN: Hanya satu status yang aktif pada satu waktu
const [status, setStatus] = useState('idle'); 
// Nilai yang mungkin: 'idle' | 'mengetik' | 'mengirim' | 'sukses' | 'error'

Dengan cara ini, MUSTAHIL status jadi 'mengirim' DAN 'sukses' bersamaan. Satu variabel, satu nilai, satu kebenaran.

Kapan Pakai Boolean vs String/Enum?

jsx
// Boolean cocok untuk hal yang benar-benar on/off
const [modalTerbuka, setModalTerbuka] = useState(false);

// String/enum cocok untuk hal yang punya BANYAK kemungkinan
const [statusPesanan, setStatusPesanan] = useState('menunggu');
// 'menunggu' | 'diproses' | 'dikirim' | 'sampai' | 'dibatalkan'

Aturan praktis: Kalau kamu punya lebih dari 2 state yang saling eksklusif (gak bisa aktif bareng), JANGAN pakai multiple boolean. Pakai satu state dengan beberapa kemungkinan nilai.


State Machine Thinking

💡Info

Mesin minuman punya aturan ketat:

  1. Idle → masukkin uang → Punya Saldo
  2. Punya Saldo → pilih minuman → Mengeluarkan
  3. Mengeluarkan → minuman keluar → Idle

Kamu GAK BISA langsung dari Idle ke Mengeluarkan tanpa masukkin uang dulu. Ada ATURAN transisi.

Ini konsep state machine: kumpulan state + aturan perpindahan antar state.

Menerapkan di React

jsx
function MesinMinuman() {
  const [state, setState] = useState('idle');
  const [saldo, setSaldo] = useState(0);

  // Definisikan transisi yang VALID
  function masukkanUang(jumlah) {
    // Hanya bisa dari state 'idle' atau 'punya_saldo'
    if (state === 'idle' || state === 'punya_saldo') {
      setSaldo(saldo + jumlah);
      setState('punya_saldo');
    }
    // Kalau state-nya 'mengeluarkan', tombol uang diabaikan
  }

  function pilihMinuman(harga) {
    // Hanya bisa dari state 'punya_saldo' DAN saldo cukup
    if (state === 'punya_saldo' && saldo >= harga) {
      setState('mengeluarkan');
      setSaldo(saldo - harga);

      // Simulasi minuman keluar
      setTimeout(() => {
        setState('idle');
      }, 2000);
    }
  }

  return (
    <div>
      <h2>🥤 Mesin Minuman</h2>
      <p>Status: {state}</p>
      <p>Saldo: Rp {saldo.toLocaleString()}</p>

      <button
        onClick={() => masukkanUang(2000)}
        disabled={state === 'mengeluarkan'}
      >
        Masukkan Rp 2.000
      </button>

      <button
        onClick={() => pilihMinuman(5000)}
        disabled={state !== 'punya_saldo' || saldo < 5000}
      >
        Teh Botol (Rp 5.000)
      </button>

      {state === 'mengeluarkan' && <p>⏳ Mengambil minuman...</p>}
    </div>
  );
}

Kenapa State Machine Thinking Penting?

  1. Mencegah bug: Kamu gak bisa "lompat" ke state yang gak valid
  2. Mudah di-debug: Kalau ada masalah, cek aja transisi mana yang salah
  3. Mudah ditambah: Mau tambah state baru? Tinggal definisiin transisinya
  4. Dokumentasi hidup: Kode-nya sendiri menjelaskan alur aplikasi

Contoh Lengkap: Form Login

Mari kita gabungkan semua konsep di atas dalam satu contoh nyata:

jsx
import { useState } from 'react';

function FormLogin() {
  // Satu state untuk mengontrol seluruh alur
  const [status, setStatus] = useState('idle');
  // 'idle' | 'mengetik' | 'mengirim' | 'sukses' | 'error'

  // Data form
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [pesanError, setPesanError] = useState('');

  // Apakah form bisa di-submit?
  const bisaSubmit = email.length > 0 && password.length > 0 && status !== 'mengirim';

  // Handler: user ngetik
  function handleInputBerubah(field, nilai) {
    if (field === 'email') setEmail(nilai);
    if (field === 'password') setPassword(nilai);

    // Kalau sebelumnya error, reset ke mengetik
    if (status === 'error') {
      setStatus('mengetik');
      setPesanError('');
    } else if (status === 'idle') {
      setStatus('mengetik');
    }
  }

  // Handler: user submit form
  async function handleSubmit(e) {
    e.preventDefault();

    // Guard: jangan submit kalau lagi ngirim
    if (status === 'mengirim') return;

    setStatus('mengirim');

    try {
      // Simulasi API call
      await new Promise((resolve, reject) => {
        setTimeout(() => {
          if (email === 'admin@test.com' && password === '123456') {
            resolve();
          } else {
            reject(new Error('Email atau password salah'));
          }
        }, 1500);
      });

      setStatus('sukses');
    } catch (err) {
      setStatus('error');
      setPesanError(err.message);
    }
  }

  // RENDER berdasarkan state
  if (status === 'sukses') {
    return (
      <div className="sukses">
        <h2>🎉 Login Berhasil!</h2>
        <p>Selamat datang, {email}!</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>🔐 Login</h2>

      <div>
        <label>Email:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => handleInputBerubah('email', e.target.value)}
          disabled={status === 'mengirim'}
          placeholder="contoh@email.com"
        />
      </div>

      <div>
        <label>Password:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => handleInputBerubah('password', e.target.value)}
          disabled={status === 'mengirim'}
          placeholder="Masukkan password"
        />
      </div>

      {/* Tampilkan error kalau ada */}
      {status === 'error' && (
        <p style={{ color: 'red' }}>❌ {pesanError}</p>
      )}

      <button type="submit" disabled={!bisaSubmit}>
        {status === 'mengirim' ? '⏳ Memproses...' : 'Masuk'}
      </button>
    </form>
  );
}

Breakdown Alur:

[idle] ──user ngetik──→ [mengetik] ──klik submit──→ [mengirim] ↑ ↓ │ berhasil → [sukses] │ gagal → [error] └────user ngetik lagi────────────┘

Ringkasan Cara Berpikir Deklaratif

LangkahApa yang dilakukanContoh
1Identifikasi semua visual stateidle, mengetik, mengirim, sukses, error
2Tentukan apa yang memicu perpindahan stateUser ngetik, klik submit, API response
3Representasikan state dengan useStateconst [status, setStatus] = useState('idle')
4Hapus state yang gak mungkin terjadiGabungin boolean jadi satu enum
5Hubungkan event handler ke state setteronClick → setStatus('mengirim')

⚠️ Jebakan

Jebakan 1: Terlalu Banyak Boolean

jsx
// ❌ JANGAN GINI
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [success, setSuccess] = useState(false);

// Bisa terjadi: loading=true, success=true → MUSTAHIL tapi kode memungkinkan

// ✅ GINI AJA
const [status, setStatus] = useState('idle');
// 'idle' | 'loading' | 'success' | 'error'

Jebakan 2: Lupa Disable Input Saat Loading

jsx
// ❌ User bisa klik tombol berkali-kali saat loading
<button onClick={handleSubmit}>Kirim</button>

// ✅ Disable saat sedang proses
<button onClick={handleSubmit} disabled={status === 'mengirim'}>
  {status === 'mengirim' ? 'Mengirim...' : 'Kirim'}
</button>

Jebakan 3: Gak Handle State Error

jsx
// ❌ Kalau API gagal, user stuck di loading selamanya
async function handleSubmit() {
  setStatus('mengirim');
  const hasil = await kirimData(); // Kalau error, gak di-catch!
  setStatus('sukses');
}

// ✅ Selalu handle error
async function handleSubmit() {
  setStatus('mengirim');
  try {
    const hasil = await kirimData();
    setStatus('sukses');
  } catch (err) {
    setStatus('error');
    setPesanError(err.message);
  }
}

Jebakan 4: Berpikir Imperatif di React

jsx
// ❌ Mindset imperatif: "Saya mau UBAH tampilan"
function handleKlik() {
  document.getElementById('pesan').style.display = 'block'; // JANGAN!
}

// ✅ Mindset deklaratif: "Saya mau UBAH state, biar React yang ubah tampilan"
function handleKlik() {
  setTampilkanPesan(true); // React otomatis re-render
}

Jebakan 5: Impossible State yang Gak Disadari

jsx
// ❌ Dua state yang harusnya gak bisa true bersamaan
const [sedangEdit, setSedangEdit] = useState(false);
const [sedangHapus, setSedangHapus] = useState(false);

// Gimana kalau keduanya true? UI jadi bingung

// ✅ Pakai satu state
const [mode, setMode] = useState('tampil'); // 'tampil' | 'edit' | 'hapus'

🏋️ Challenge

Challenge 1: Traffic Light (Lampu Lalu Lintas)

Bikin komponen lampu lalu lintas yang otomatis berganti setiap beberapa detik:

  • Merah (5 detik) → Kuning (2 detik) → Hijau (5 detik) → Kuning (2 detik) → Merah...

Hint: Pakai useState untuk state warna dan useEffect dengan setTimeout untuk perpindahan otomatis.

Lihat Solusi
jsx
import { useState, useEffect } from 'react';

function LampuLaluLintas() {
  const [warna, setWarna] = useState('merah');

  useEffect(() => {
    let durasi;

    // Tentukan durasi berdasarkan warna saat ini
    if (warna === 'merah') durasi = 5000;
    else if (warna === 'kuning') durasi = 2000;
    else durasi = 5000; // hijau

    const timer = setTimeout(() => {
      // Tentukan warna berikutnya
      if (warna === 'merah') setWarna('hijau');
      else if (warna === 'hijau') setWarna('kuning');
      else setWarna('merah'); // dari kuning ke merah
    }, durasi);

    // Cleanup timer kalau komponen di-unmount
    return () => clearTimeout(timer);
  }, [warna]); // Jalankan ulang setiap warna berubah

  const warnaCSS = {
    merah: '#ff0000',
    kuning: '#ffcc00',
    hijau: '#00cc00',
  };

  return (
    <div style={{ textAlign: 'center', padding: '20px' }}>
      <h2>🚦 Lampu Lalu Lintas</h2>
      <div
        style={{
          width: '100px',
          height: '100px',
          borderRadius: '50%',
          backgroundColor: warnaCSS[warna],
          margin: '20px auto',
          border: '3px solid #333',
        }}
      />
      <p>Status: {warna.toUpperCase()}</p>
    </div>
  );
}

Challenge 2: Form Multi-Step

Bikin form pendaftaran dengan 3 langkah:

  1. Isi nama & email
  2. Isi alamat
  3. Konfirmasi & kirim

User bisa maju dan mundur antar langkah. Tombol "Kirim" cuma muncul di langkah 3.

Hint: Pakai state langkah (1, 2, atau 3) dan render form yang berbeda berdasarkan langkah.

Lihat Solusi
jsx
import { useState } from 'react';

function FormPendaftaran() {
  const [langkah, setLangkah] = useState(1);
  const [data, setData] = useState({
    nama: '',
    email: '',
    alamat: '',
    kota: '',
  });
  const [status, setStatus] = useState('mengisi'); // 'mengisi' | 'mengirim' | 'sukses'

  function updateData(field, nilai) {
    setData({ ...data, [field]: nilai });
  }

  function maju() {
    if (langkah < 3) setLangkah(langkah + 1);
  }

  function mundur() {
    if (langkah > 1) setLangkah(langkah - 1);
  }

  async function handleKirim() {
    setStatus('mengirim');
    // Simulasi API
    await new Promise(resolve => setTimeout(resolve, 1500));
    setStatus('sukses');
  }

  if (status === 'sukses') {
    return <h2>🎉 Pendaftaran berhasil! Selamat datang, {data.nama}!</h2>;
  }

  return (
    <div>
      <h2>📝 Pendaftaran (Langkah {langkah}/3)</h2>

      {/* Progress bar sederhana */}
      <div style={{ display: 'flex', gap: '5px', marginBottom: '20px' }}>
        {[1, 2, 3].map(l => (
          <div
            key={l}
            style={{
              flex: 1,
              height: '8px',
              backgroundColor: l <= langkah ? '#4CAF50' : '#ddd',
              borderRadius: '4px',
            }}
          />
        ))}
      </div>

      {/* Langkah 1: Data Diri */}
      {langkah === 1 && (
        <div>
          <h3>Data Diri</h3>
          <input
            placeholder="Nama lengkap"
            value={data.nama}
            onChange={(e) => updateData('nama', e.target.value)}
          />
          <input
            placeholder="Email"
            type="email"
            value={data.email}
            onChange={(e) => updateData('email', e.target.value)}
          />
        </div>
      )}

      {/* Langkah 2: Alamat */}
      {langkah === 2 && (
        <div>
          <h3>Alamat</h3>
          <input
            placeholder="Alamat lengkap"
            value={data.alamat}
            onChange={(e) => updateData('alamat', e.target.value)}
          />
          <input
            placeholder="Kota"
            value={data.kota}
            onChange={(e) => updateData('kota', e.target.value)}
          />
        </div>
      )}

      {/* Langkah 3: Konfirmasi */}
      {langkah === 3 && (
        <div>
          <h3>Konfirmasi Data</h3>
          <p><strong>Nama:</strong> {data.nama}</p>
          <p><strong>Email:</strong> {data.email}</p>
          <p><strong>Alamat:</strong> {data.alamat}</p>
          <p><strong>Kota:</strong> {data.kota}</p>
        </div>
      )}

      {/* Tombol navigasi */}
      <div style={{ marginTop: '20px' }}>
        {langkah > 1 && (
          <button onClick={mundur} disabled={status === 'mengirim'}>
            ← Kembali
          </button>
        )}
        {langkah < 3 && (
          <button onClick={maju}>
            Lanjut →
          </button>
        )}
        {langkah === 3 && (
          <button onClick={handleKirim} disabled={status === 'mengirim'}>
            {status === 'mengirim' ? '⏳ Mengirim...' : '✅ Kirim Pendaftaran'}
          </button>
        )}
      </div>
    </div>
  );
}

Challenge 3: Quiz App dengan State Machine

Bikin aplikasi quiz sederhana dengan state: belum_mulai → menjawab → hasil

  • Di state belum_mulai: tampilkan tombol "Mulai Quiz"
  • Di state menjawab: tampilkan pertanyaan + pilihan jawaban
  • Di state hasil: tampilkan skor

Hint: Simpan juga state untuk soalKe (index soal saat ini) dan skor.

Lihat Solusi
jsx
import { useState } from 'react';

const daftarSoal = [
  {
    pertanyaan: 'Apa ibukota Indonesia?',
    pilihan: ['Bandung', 'Jakarta', 'Surabaya', 'Yogyakarta'],
    jawabanBenar: 1, // index
  },
  {
    pertanyaan: 'React dibuat oleh perusahaan apa?',
    pilihan: ['Google', 'Microsoft', 'Meta (Facebook)', 'Apple'],
    jawabanBenar: 2,
  },
  {
    pertanyaan: 'Berapa hasil dari 2 + 2?',
    pilihan: ['3', '4', '5', '22'],
    jawabanBenar: 1,
  },
];

function QuizApp() {
  const [fase, setFase] = useState('belum_mulai'); // 'belum_mulai' | 'menjawab' | 'hasil'
  const [soalKe, setSoalKe] = useState(0);
  const [skor, setSkor] = useState(0);

  function mulaiQuiz() {
    setFase('menjawab');
    setSoalKe(0);
    setSkor(0);
  }

  function jawab(indexPilihan) {
    // Cek jawaban
    if (indexPilihan === daftarSoal[soalKe].jawabanBenar) {
      setSkor(skor + 1);
    }

    // Pindah ke soal berikutnya atau selesai
    if (soalKe + 1 < daftarSoal.length) {
      setSoalKe(soalKe + 1);
    } else {
      setFase('hasil');
    }
  }

  // STATE: Belum mulai
  if (fase === 'belum_mulai') {
    return (
      <div>
        <h2>🧠 Quiz Time!</h2>
        <p>Ada {daftarSoal.length} soal. Siap?</p>
        <button onClick={mulaiQuiz}>Mulai Quiz</button>
      </div>
    );
  }

  // STATE: Menjawab
  if (fase === 'menjawab') {
    const soal = daftarSoal[soalKe];
    return (
      <div>
        <h2>Soal {soalKe + 1}/{daftarSoal.length}</h2>
        <p><strong>{soal.pertanyaan}</strong></p>
        {soal.pilihan.map((pilihan, index) => (
          <button
            key={index}
            onClick={() => jawab(index)}
            style={{ display: 'block', margin: '8px 0', padding: '10px' }}
          >
            {pilihan}
          </button>
        ))}
      </div>
    );
  }

  // STATE: Hasil
  return (
    <div>
      <h2>🏆 Hasil Quiz</h2>
      <p>Skor kamu: {skor}/{daftarSoal.length}</p>
      <p>
        {skor === daftarSoal.length
          ? '🎉 Sempurna!'
          : skor >= daftarSoal.length / 2
          ? '👍 Lumayan!'
          : '📚 Belajar lagi ya!'}
      </p>
      <button onClick={mulaiQuiz}>Ulangi Quiz</button>
    </div>
  );
}

Kesimpulan

Berpikir deklaratif itu kayak jadi sutradara film. Kamu gak perlu gerakin tangan aktor satu per satu. Kamu cuma bilang: "Di scene ini, kamu sedih. Di scene itu, kamu marah." Aktor (React) yang eksekusi detailnya.

Yang kamu pelajari di bab ini:

  1. UI deklaratif = kamu deskripsikan "apa yang harus ditampilkan", bukan "bagaimana mengubahnya"
  2. Identifikasi semua visual state sebelum nulis kode
  3. Gunakan satu state (enum/string) untuk hal yang saling eksklusif
  4. Hubungkan event handler ke state transition
  5. Hindari impossible state dengan menggabungkan boolean yang berkaitan
  6. State machine thinking membantu bikin alur yang jelas dan bebas bug

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.