Bab 6: Conditional Rendering

5 menit baca

Tampilkan Sesuai Kondisi

Bayangin kamu buka aplikasi ojol. Kalau kamu belum login, yang muncul halaman login. Kalau udah login, yang muncul halaman beranda. Kalau pesanan lagi diantar, muncul tracking. Kalau udah sampai, muncul tombol "Beri Rating."

Setiap kondisi berbeda menampilkan hal yang berbeda. Inilah conditional rendering: menampilkan komponen atau elemen yang berbeda berdasarkan kondisi tertentu.

Di React, kamu pakai fitur JavaScript biasa (if, ternary, &&) untuk menentukan apa yang ditampilkan. Nggak ada sintaks khusus. Ini salah satu hal yang bikin React powerful: kamu cuma perlu tahu JavaScript.

Cara 1: if/else di Luar JSX

Cara paling straightforward. Tulis logika if/else sebelum return, tentukan apa yang mau di-return.

jsx
function StatusPesanan({ status }) {
  // Logika di luar JSX
  if (status === 'diproses') {
    return (
      <div className="status diproses">
        <span>🔄</span>
        <p>Pesanan kamu sedang diproses...</p>
      </div>
    );
  }
  
  if (status === 'dikirim') {
    return (
      <div className="status dikirim">
        <span>🚚</span>
        <p>Pesanan sedang dalam perjalanan!</p>
      </div>
    );
  }
  
  if (status === 'selesai') {
    return (
      <div className="status selesai">
        <span>✅</span>
        <p>Pesanan sudah sampai. Selamat menikmati!</p>
      </div>
    );
  }
  
  // Default kalau status nggak dikenal
  return (
    <div className="status unknown">
      <span>❓</span>
      <p>Status tidak diketahui</p>
    </div>
  );
}

Kapan pakai cara ini?

  • Ketika setiap kondisi me-return JSX yang sangat berbeda (bukan cuma beda sedikit)
  • Ketika ada banyak kondisi (lebih dari 2-3)
  • Ketika logikanya kompleks

Pattern: Early Return

Early return itu pattern di mana kamu "pulang duluan" kalau kondisi tertentu terpenuhi, tanpa perlu else:

jsx
function KontenHalaman({ loading, error, data }) {
  // Early return untuk loading
  if (loading) {
    return <div className="spinner">Memuat data...</div>;
  }
  
  // Early return untuk error
  if (error) {
    return <div className="error">Terjadi kesalahan: {error.message}</div>;
  }
  
  // Early return kalau data kosong
  if (!data || data.length === 0) {
    return <div className="empty">Belum ada data.</div>;
  }
  
  // Kalau semua pengecekan lolos, tampilkan konten utama
  return (
    <div className="konten">
      <h1>Data Berhasil Dimuat!</h1>
      <p>Jumlah item: {data.length}</p>
      {/* ... render data ... */}
    </div>
  );
}

Kenapa early return bagus? Karena kamu nggak perlu nested if/else yang bikin kode susah dibaca. Setiap kondisi "khusus" ditangani di awal, dan kode utama ada di paling bawah tanpa indentasi berlebihan.

Cara 2: Ternary Operator (? :)

Ternary operator bisa dipakai di dalam JSX. Ini berguna kalau kamu mau render salah satu dari dua opsi.

Sintaks: kondisi ? tampilKalauTrue : tampilKalauFalse

jsx
function Sapaan({ sudahLogin, nama }) {
  return (
    <div>
      <h1>
        {sudahLogin 
          ? `Selamat datang kembali, ${nama}!` 
          : 'Silakan login terlebih dahulu'}
      </h1>
      
      {sudahLogin 
        ? <button>Logout</button> 
        : <button>Login</button>
      }
    </div>
  );
}

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

Ternary untuk Elemen yang Lebih Kompleks

jsx
function KartuProduk({ nama, harga, stok }) {
  return (
    <div className="kartu-produk">
      <h3>{nama}</h3>
      <p>Rp {harga.toLocaleString('id-ID')}</p>
      
      {stok > 0 ? (
        // Kalau stok ada
        <div className="tersedia">
          <p className="stok">Stok: {stok} item</p>
          <button className="btn-beli">
            🛒 Tambah ke Keranjang
          </button>
        </div>
      ) : (
        // Kalau stok habis
        <div className="habis">
          <p className="stok-habis">❌ Stok Habis</p>
          <button className="btn-notif" disabled>
            🔔 Notifikasi Saat Tersedia
          </button>
        </div>
      )}
    </div>
  );
}

Kapan pakai ternary?

  • Ketika cuma ada dua kemungkinan (true/false)
  • Ketika perbedaannya nggak terlalu besar
  • Ketika mau inline di dalam JSX

Cara 3: Operator && (Logical AND)

Operator && berguna kalau kamu mau tampilkan sesuatu ATAU nggak tampilkan apa-apa. Nggak ada "else"-nya.

Cara kerjanya: kondisi && <Elemen /> artinya "kalau kondisi true, tampilkan elemen. Kalau false, nggak tampilkan apa-apa."

jsx
function Notifikasi({ jumlahPesan, isPremium }) {
  return (
    <div>
      <h1>Dashboard</h1>
      
      {/* Tampilkan badge notifikasi kalau ada pesan */}
      {jumlahPesan > 0 && (
        <span className="badge">
          📬 {jumlahPesan} pesan baru
        </span>
      )}
      
      {/* Tampilkan badge premium kalau user premium */}
      {isPremium && (
        <span className="badge-premium">
          ⭐ Premium Member
        </span>
      )}
      
      {/* Tampilkan warning kalau pesan banyak banget */}
      {jumlahPesan > 100 && (
        <div className="warning">
          ⚠️ Kamu punya banyak pesan belum dibaca!
        </div>
      )}
    </div>
  );
}

Kenapa && Bisa Gitu?

Ini fitur JavaScript, bukan React. Operator && mengevaluasi dari kiri ke kanan:

  • Kalau sisi kiri falsy (false, null, undefined, 0, ""), langsung return sisi kiri
  • Kalau sisi kiri truthy, return sisi kanan
javascript
// JavaScript biasa:
true && "Halo"     // → "Halo" (sisi kiri truthy, return kanan)
false && "Halo"    // → false (sisi kiri falsy, return kiri)
null && "Halo"     // → null
5 > 3 && "Ya"      // → "Ya"
5 < 3 && "Ya"      // → false

React tahu cara handle false, null, undefined: nggak render apa-apa. Makanya pattern ini jalan.

⚠️ HATI-HATI dengan Angka 0!

Ini jebakan klasik yang bikin banyak developer bingung:

jsx
function DaftarPesan({ pesan }) {
  return (
    <div>
      {/* ❌ BUG! Kalau pesan.length = 0, akan MENAMPILKAN angka "0" */}
      {pesan.length && <p>Kamu punya {pesan.length} pesan</p>}
      
      {/* ✅ BENAR - konversi ke boolean dulu */}
      {pesan.length > 0 && <p>Kamu punya {pesan.length} pesan</p>}
      
      {/* ✅ BENAR - pakai Boolean() */}
      {Boolean(pesan.length) && <p>Kamu punya {pesan.length} pesan</p>}
      
      {/* ✅ BENAR - pakai double negation */}
      {!!pesan.length && <p>Kamu punya {pesan.length} pesan</p>}
    </div>
  );
}

Kenapa? Karena 0 && <p>...</p> menghasilkan 0 (bukan false). Dan React menampilkan angka 0 di layar! Jadi kamu lihat angka "0" muncul entah dari mana.

Aturan aman: Selalu pastikan sisi kiri && menghasilkan boolean (true/false), bukan angka.

Cara 4: Menyimpan JSX di Variabel

Kadang logikanya terlalu kompleks untuk ternary tapi kamu nggak mau multiple return. Solusinya: simpan JSX di variabel.

jsx
function DashboardUser({ user }) {
  // Tentukan konten berdasarkan role
  let kontenDashboard;
  
  if (user.role === 'admin') {
    kontenDashboard = (
      <div className="admin-panel">
        <h2>Panel Admin</h2>
        <button>Kelola User</button>
        <button>Lihat Laporan</button>
        <button>Pengaturan Sistem</button>
      </div>
    );
  } else if (user.role === 'seller') {
    kontenDashboard = (
      <div className="seller-panel">
        <h2>Panel Penjual</h2>
        <button>Tambah Produk</button>
        <button>Lihat Pesanan</button>
        <button>Statistik Penjualan</button>
      </div>
    );
  } else {
    kontenDashboard = (
      <div className="buyer-panel">
        <h2>Hai, {user.nama}!</h2>
        <button>Riwayat Belanja</button>
        <button>Wishlist</button>
      </div>
    );
  }
  
  // Tentukan greeting berdasarkan waktu
  const jam = new Date().getHours();
  let sapaan;
  if (jam < 12) sapaan = "Selamat Pagi";
  else if (jam < 17) sapaan = "Selamat Siang";
  else sapaan = "Selamat Malam";
  
  return (
    <div className="dashboard">
      <header>
        <h1>{sapaan}, {user.nama}! 👋</h1>
      </header>
      {kontenDashboard}
    </div>
  );
}

Cara 5: Return null untuk Sembunyikan Komponen

Kalau kamu mau komponen nggak render apa-apa sama sekali, return null:

jsx
function BannerPromo({ tampilkan, pesan }) {
  // Kalau nggak perlu ditampilkan, return null
  if (!tampilkan) {
    return null;  // Komponen ini "menghilang" dari DOM
  }
  
  return (
    <div className="banner-promo">
      <p>🎉 {pesan}</p>
      <button>Lihat Promo</button>
    </div>
  );
}

function App() {
  const adaPromo = true;
  
  return (
    <div>
      <BannerPromo tampilkan={adaPromo} pesan="Diskon 50% hari ini!" />
      <h1>Selamat Datang</h1>
    </div>
  );
}

Catatan: Return null berarti komponen tetap "ada" di tree React, tapi nggak menghasilkan output DOM apapun. Ini beda dengan nggak me-render komponen sama sekali.

jsx
// Dua cara "sembunyikan" - hasilnya mirip tapi beda secara teknis:

// Cara 1: Komponen return null (komponen tetap di-mount)
<BannerPromo tampilkan={false} pesan="..." />

// Cara 2: Nggak render komponen sama sekali (komponen di-unmount)
{adaPromo && <BannerPromo pesan="..." />}

Menangani Banyak Kondisi

Pattern: Object Mapping

Kalau kamu punya banyak kondisi yang masing-masing render hal berbeda, object mapping lebih rapi dari if/else bertingkat:

jsx
function IconStatus({ status }) {
  // Object mapping - lebih rapi dari switch/if-else
  const statusConfig = {
    pending: { icon: '⏳', teks: 'Menunggu', warna: '#ff9800' },
    processing: { icon: '🔄', teks: 'Diproses', warna: '#2196f3' },
    shipped: { icon: '🚚', teks: 'Dikirim', warna: '#9c27b0' },
    delivered: { icon: '✅', teks: 'Sampai', warna: '#4caf50' },
    cancelled: { icon: '❌', teks: 'Dibatalkan', warna: '#f44336' },
  };
  
  const config = statusConfig[status] || statusConfig.pending;
  
  return (
    <span style={{ 
      color: config.warna, 
      fontWeight: 'bold',
      padding: '4px 8px',
      borderRadius: '4px',
      backgroundColor: `${config.warna}20`
    }}>
      {config.icon} {config.teks}
    </span>
  );
}

Pattern: Switch Statement (di Luar JSX)

jsx
function HalamanAuth({ mode }) {
  switch (mode) {
    case 'login':
      return (
        <div>
          <h1>Login</h1>
          <form>
            <input type="email" placeholder="Email" />
            <input type="password" placeholder="Password" />
            <button>Masuk</button>
          </form>
        </div>
      );
    
    case 'register':
      return (
        <div>
          <h1>Daftar Akun Baru</h1>
          <form>
            <input type="text" placeholder="Nama Lengkap" />
            <input type="email" placeholder="Email" />
            <input type="password" placeholder="Password" />
            <input type="password" placeholder="Konfirmasi Password" />
            <button>Daftar</button>
          </form>
        </div>
      );
    
    case 'forgot':
      return (
        <div>
          <h1>Lupa Password</h1>
          <form>
            <input type="email" placeholder="Email terdaftar" />
            <button>Kirim Link Reset</button>
          </form>
        </div>
      );
    
    default:
      return <p>Halaman tidak ditemukan</p>;
  }
}

Hindari: Nested Ternary yang Terlalu Dalam

jsx
// ❌ Susah dibaca - nested ternary 3 level
function Status({ level }) {
  return (
    <p>
      {level === 'gold' 
        ? '🥇 Gold Member' 
        : level === 'silver' 
          ? '🥈 Silver Member' 
          : level === 'bronze' 
            ? '🥉 Bronze Member' 
            : '👤 Regular Member'}
    </p>
  );
}

// ✅ Lebih baik - pakai object mapping atau fungsi helper
function Status({ level }) {
  const labels = {
    gold: '🥇 Gold Member',
    silver: '🥈 Silver Member',
    bronze: '🥉 Bronze Member',
  };
  
  return <p>{labels[level] || '👤 Regular Member'}</p>;
}

Conditional Rendering dengan CSS (Alternatif)

Kadang lebih baik pakai CSS untuk show/hide daripada conditional rendering:

jsx
// Cara React: conditional rendering (elemen dihapus/ditambah dari DOM)
function Dropdown({ buka, children }) {
  return (
    <div className="dropdown">
      {buka && <div className="dropdown-menu">{children}</div>}
    </div>
  );
}

// Cara CSS: hide/show (elemen tetap di DOM, cuma disembunyikan)
function Dropdown({ buka, children }) {
  return (
    <div className="dropdown">
      <div 
        className="dropdown-menu"
        style={{ display: buka ? 'block' : 'none' }}
      >
        {children}
      </div>
    </div>
  );
}

Kapan pakai mana?

  • Conditional rendering (React): Kalau elemen jarang muncul, atau kalau elemen berat (banyak child). Lebih hemat memori.
  • CSS display/visibility: Kalau elemen sering toggle (buka-tutup), atau kalau kamu mau animasi transisi. Lebih cepat karena nggak perlu re-mount.

Contoh Lengkap: Halaman E-Commerce

jsx
function HalamanProduk({ produk, user }) {
  // Early return untuk loading/error
  if (!produk) {
    return <div className="loading">Memuat produk...</div>;
  }
  
  // Hitung diskon
  const adaDiskon = produk.diskon > 0;
  const hargaFinal = adaDiskon 
    ? produk.harga - (produk.harga * produk.diskon / 100) 
    : produk.harga;
  
  // Tentukan label stok
  let labelStok;
  if (produk.stok === 0) {
    labelStok = <span style={{ color: 'red' }}>❌ Habis</span>;
  } else if (produk.stok <= 5) {
    labelStok = <span style={{ color: 'orange' }}>⚠️ Sisa {produk.stok}</span>;
  } else {
    labelStok = <span style={{ color: 'green' }}>✅ Tersedia</span>;
  }
  
  return (
    <div className="halaman-produk">
      {/* Banner diskon - hanya muncul kalau ada diskon */}
      {adaDiskon && (
        <div className="banner-diskon">
          🔥 DISKON {produk.diskon}%! Hemat Rp {(produk.harga * produk.diskon / 100).toLocaleString('id-ID')}
        </div>
      )}
      
      {/* Gambar produk */}
      <img src={produk.gambar} alt={produk.nama} />
      
      {/* Info produk */}
      <div className="info">
        <h1>{produk.nama}</h1>
        
        {/* Harga - tampilan beda kalau ada diskon */}
        {adaDiskon ? (
          <div className="harga-wrapper">
            <span className="harga-coret">
              Rp {produk.harga.toLocaleString('id-ID')}
            </span>
            <span className="harga-diskon">
              Rp {hargaFinal.toLocaleString('id-ID')}
            </span>
            <span className="badge-diskon">-{produk.diskon}%</span>
          </div>
        ) : (
          <span className="harga">
            Rp {produk.harga.toLocaleString('id-ID')}
          </span>
        )}
        
        {/* Stok */}
        <p>Stok: {labelStok}</p>
        
        {/* Rating - hanya tampil kalau ada review */}
        {produk.jumlahReview > 0 && (
          <p>
            {'⭐'.repeat(Math.round(produk.rating))} 
            ({produk.jumlahReview} review)
          </p>
        )}
        
        {/* Tombol beli - disabled kalau stok habis */}
        <button 
          disabled={produk.stok === 0}
          className={produk.stok === 0 ? 'btn-disabled' : 'btn-beli'}
        >
          {produk.stok === 0 ? 'Stok Habis' : '🛒 Beli Sekarang'}
        </button>
        
        {/* Pesan khusus untuk user yang belum login */}
        {!user && (
          <p className="info-login">
            💡 Login dulu untuk bisa beli dan dapat cashback!
          </p>
        )}
        
        {/* Badge seller terpercaya */}
        {produk.seller.verified && (
          <div className="seller-badge">
            ✓ Seller Terpercaya | {produk.seller.nama}
          </div>
        )}
      </div>
    </div>
  );
}

⚠️ Jebakan

Jebakan 1: Angka 0 dengan &&

jsx
// ❌ BUG - menampilkan "0" di layar!
function Keranjang({ items }) {
  return (
    <div>
      {items.length && <p>{items.length} item di keranjang</p>}
    </div>
  );
}
// Kalau items = [], items.length = 0, dan 0 ditampilkan React!

// ✅ FIX - pastikan hasilnya boolean
function Keranjang({ items }) {
  return (
    <div>
      {items.length > 0 && <p>{items.length} item di keranjang</p>}
    </div>
  );
}

Jebakan 2: if di Dalam JSX

jsx
// ❌ SALAH - if/else nggak bisa di dalam JSX
function Komponen({ aktif }) {
  return (
    <div>
      {if (aktif) { <p>Aktif</p> }}  // Syntax Error!
    </div>
  );
}

// ✅ BENAR - pakai ternary atau &&
function Komponen({ aktif }) {
  return (
    <div>
      {aktif ? <p>Aktif</p> : <p>Nonaktif</p>}
    </div>
  );
}

Jebakan 3: Lupa Kurung di Ternary Multi-baris

jsx
// ❌ Bisa error atau hasil nggak sesuai
function Komponen({ tampil }) {
  return (
    <div>
      {tampil ? 
        <h1>Judul</h1>
        <p>Paragraf</p>  // Error! Ternary cuma bisa return SATU ekspresi
      : null}
    </div>
  );
}

// ✅ Bungkus dengan Fragment atau div
function Komponen({ tampil }) {
  return (
    <div>
      {tampil ? (
        <>
          <h1>Judul</h1>
          <p>Paragraf</p>
        </>
      ) : null}
    </div>
  );
}

Jebakan 4: Ternary yang Terlalu Kompleks

jsx
// ❌ Susah dibaca dan maintain
<div>
  {isLoading ? <Spinner /> : error ? <Error msg={error} /> : data ? <Content data={data} /> : <Empty />}
</div>

// ✅ Lebih baik pakai early return atau variabel
function Komponen({ isLoading, error, data }) {
  if (isLoading) return <Spinner />;
  if (error) return <Error msg={error} />;
  if (!data) return <Empty />;
  return <Content data={data} />;
}

Jebakan 5: Conditional Rendering vs Conditional Props

jsx
// Ini CONDITIONAL RENDERING - elemen muncul/hilang
{isPremium && <Badge teks="Premium" />}

// Ini CONDITIONAL PROPS - elemen selalu ada, props-nya yang berubah
<Badge teks={isPremium ? "Premium" : "Free"} />
<button className={aktif ? "btn-aktif" : "btn-nonaktif"}>Klik</button>

// Jangan campur aduk! Pilih yang sesuai kebutuhan.

Ringkasan Kapan Pakai Apa

TeknikKapan PakaiContoh
if/else + returnBanyak kondisi, JSX sangat berbedaHalaman loading/error/sukses
Early returnHandle kasus khusus di awalGuard clause untuk null/loading
Ternary ? :Dua opsi, inline di JSXToggle teks/elemen
&&Tampil atau nggak (tanpa else)Badge, notifikasi, warning
Variabel JSXLogika kompleks sebelum renderDashboard multi-role
return nullKomponen kadang nggak perlu renderBanner promo
Object mappingBanyak kondisi dengan pola serupaStatus icon, label

🏋️ Challenge

Challenge 1: Komponen Status Pengiriman

Buat komponen StatusPengiriman yang menerima prop status dengan nilai: "dikemas", "dikirim", "transit", "sampai". Tampilkan:

  • Icon yang berbeda untuk setiap status
  • Progress bar visual (pakai div dengan width berbeda)
  • Teks deskripsi yang berbeda
  • Warna yang berbeda
💡 Hint
  • Pakai object mapping untuk config setiap status
  • Progress bar: div di dalam div, inner div width-nya berubah (25%, 50%, 75%, 100%)
  • Warna bisa ditaruh di inline style
✅ Solusi
jsx
function StatusPengiriman({ status }) {
  const config = {
    dikemas: { 
      icon: '📦', 
      teks: 'Pesanan sedang dikemas', 
      progress: 25, 
      warna: '#ff9800' 
    },
    dikirim: { 
      icon: '🚚', 
      teks: 'Paket sudah dijemput kurir', 
      progress: 50, 
      warna: '#2196f3' 
    },
    transit: { 
      icon: '🏢', 
      teks: 'Paket di gudang transit', 
      progress: 75, 
      warna: '#9c27b0' 
    },
    sampai: { 
      icon: '✅', 
      teks: 'Paket sudah diterima!', 
      progress: 100, 
      warna: '#4caf50' 
    },
  };
  
  const info = config[status];
  
  if (!info) {
    return <p>Status tidak dikenal: {status}</p>;
  }
  
  return (
    <div style={{ padding: '16px', border: '1px solid #ddd', borderRadius: '8px' }}>
      {/* Icon dan teks */}
      <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
        <span style={{ fontSize: '2rem' }}>{info.icon}</span>
        <div>
          <strong style={{ color: info.warna }}>{status.toUpperCase()}</strong>
          <p style={{ margin: 0, color: '#666' }}>{info.teks}</p>
        </div>
      </div>
      
      {/* Progress bar */}
      <div style={{ 
        width: '100%', 
        height: '8px', 
        backgroundColor: '#eee', 
        borderRadius: '4px',
        overflow: 'hidden'
      }}>
        <div style={{
          width: `${info.progress}%`,
          height: '100%',
          backgroundColor: info.warna,
          borderRadius: '4px',
          transition: 'width 0.5s ease'
        }} />
      </div>
      
      {/* Persentase */}
      <p style={{ textAlign: 'right', fontSize: '0.8rem', color: '#999', marginTop: '4px' }}>
        {info.progress}%
      </p>
      
      {/* Pesan tambahan kalau sudah sampai */}
      {status === 'sampai' && (
        <div style={{ 
          marginTop: '12px', 
          padding: '8px', 
          backgroundColor: '#e8f5e9', 
          borderRadius: '4px' 
        }}>
          🎉 Jangan lupa beri rating ya!
        </div>
      )}
    </div>
  );
}

// Penggunaan:
function App() {
  return (
    <div>
      <StatusPengiriman status="dikemas" />
      <StatusPengiriman status="dikirim" />
      <StatusPengiriman status="transit" />
      <StatusPengiriman status="sampai" />
    </div>
  );
}

Challenge 2: Form Login/Register Toggle

Buat komponen FormAuth yang:

  • Punya dua mode: "login" dan "register"
  • Mode login: tampilkan field email + password + tombol "Masuk"
  • Mode register: tampilkan field nama + email + password + konfirmasi password + tombol "Daftar"
  • Ada link di bawah untuk switch mode ("Belum punya akun? Daftar" / "Sudah punya akun? Masuk")
  • Pakai useState untuk toggle mode
💡 Hint
  • const [mode, setMode] = useState('login')
  • Ternary untuk switch antara form login dan register
  • onClick di link untuk setMode('register') atau setMode('login')
✅ Solusi
jsx
import { useState } from 'react';

function FormAuth() {
  const [mode, setMode] = useState('login');
  
  return (
    <div style={{ 
      maxWidth: '400px', 
      margin: '40px auto', 
      padding: '24px',
      border: '1px solid #ddd',
      borderRadius: '12px'
    }}>
      <h2 style={{ textAlign: 'center' }}>
        {mode === 'login' ? '🔐 Masuk' : '📝 Daftar Akun'}
      </h2>
      
      <form onSubmit={(e) => e.preventDefault()}>
        {/* Field nama - hanya muncul di mode register */}
        {mode === 'register' && (
          <div style={{ marginBottom: '12px' }}>
            <label>Nama Lengkap</label>
            <input 
              type="text" 
              placeholder="Masukkan nama lengkap"
              style={{ width: '100%', padding: '8px', marginTop: '4px' }}
            />
          </div>
        )}
        
        {/* Field email - selalu muncul */}
        <div style={{ marginBottom: '12px' }}>
          <label>Email</label>
          <input 
            type="email" 
            placeholder="contoh@email.com"
            style={{ width: '100%', padding: '8px', marginTop: '4px' }}
          />
        </div>
        
        {/* Field password - selalu muncul */}
        <div style={{ marginBottom: '12px' }}>
          <label>Password</label>
          <input 
            type="password" 
            placeholder="Minimal 8 karakter"
            style={{ width: '100%', padding: '8px', marginTop: '4px' }}
          />
        </div>
        
        {/* Konfirmasi password - hanya di register */}
        {mode === 'register' && (
          <div style={{ marginBottom: '12px' }}>
            <label>Konfirmasi Password</label>
            <input 
              type="password" 
              placeholder="Ulangi password"
              style={{ width: '100%', padding: '8px', marginTop: '4px' }}
            />
          </div>
        )}
        
        {/* Tombol submit */}
        <button style={{
          width: '100%',
          padding: '12px',
          backgroundColor: '#1976d2',
          color: 'white',
          border: 'none',
          borderRadius: '6px',
          cursor: 'pointer',
          fontSize: '1rem',
          marginTop: '8px'
        }}>
          {mode === 'login' ? 'Masuk' : 'Daftar'}
        </button>
      </form>
      
      {/* Link toggle mode */}
      <p style={{ textAlign: 'center', marginTop: '16px' }}>
        {mode === 'login' ? (
          <span>
            Belum punya akun?{' '}
            <a 
              href="#" 
              onClick={(e) => { e.preventDefault(); setMode('register'); }}
              style={{ color: '#1976d2' }}
            >
              Daftar di sini
            </a>
          </span>
        ) : (
          <span>
            Sudah punya akun?{' '}
            <a 
              href="#" 
              onClick={(e) => { e.preventDefault(); setMode('login'); }}
              style={{ color: '#1976d2' }}
            >
              Masuk di sini
            </a>
          </span>
        )}
      </p>
    </div>
  );
}

Challenge 3: Komponen Notifikasi Multi-Tipe

Buat komponen PusatNotifikasi yang:

  • Menerima array notifikasi dengan tipe berbeda: "info", "sukses", "warning", "error"
  • Setiap tipe punya icon, warna, dan style berbeda
  • Kalau array kosong, tampilkan pesan "Tidak ada notifikasi"
  • Ada tombol "Tandai Dibaca" di setiap notifikasi (pakai state untuk hide)
  • Tampilkan counter "X notifikasi belum dibaca" di atas
💡 Hint
  • useState dengan array ID notifikasi yang sudah dibaca
  • Filter notifikasi yang belum dibaca: notifikasi.filter(n => !dibaca.includes(n.id))
  • Object mapping untuk style setiap tipe
  • && untuk conditional counter
✅ Solusi
jsx
import { useState } from 'react';

function PusatNotifikasi({ notifikasi }) {
  const [dibaca, setDibaca] = useState([]);
  
  // Config untuk setiap tipe
  const tipeConfig = {
    info: { icon: 'ℹ️', bg: '#e3f2fd', border: '#2196f3' },
    sukses: { icon: '✅', bg: '#e8f5e9', border: '#4caf50' },
    warning: { icon: '⚠️', bg: '#fff3e0', border: '#ff9800' },
    error: { icon: '❌', bg: '#ffebee', border: '#f44336' },
  };
  
  // Filter yang belum dibaca
  const belumDibaca = notifikasi.filter(n => !dibaca.includes(n.id));
  
  function tandaiDibaca(id) {
    setDibaca([...dibaca, id]);
  }
  
  // Kalau nggak ada notifikasi sama sekali
  if (notifikasi.length === 0) {
    return (
      <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
        <p style={{ fontSize: '2rem' }}>🔔</p>
        <p>Tidak ada notifikasi</p>
      </div>
    );
  }
  
  return (
    <div style={{ maxWidth: '500px' }}>
      {/* Counter */}
      {belumDibaca.length > 0 ? (
        <p style={{ fontWeight: 'bold', marginBottom: '12px' }}>
          🔔 {belumDibaca.length} notifikasi belum dibaca
        </p>
      ) : (
        <p style={{ color: '#999', marginBottom: '12px' }}>
          ✓ Semua notifikasi sudah dibaca
        </p>
      )}
      
      {/* Daftar notifikasi */}
      {belumDibaca.map(notif => {
        const config = tipeConfig[notif.tipe] || tipeConfig.info;
        
        return (
          <div 
            key={notif.id}
            style={{
              padding: '12px 16px',
              marginBottom: '8px',
              backgroundColor: config.bg,
              borderLeft: `4px solid ${config.border}`,
              borderRadius: '4px',
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center'
            }}
          >
            <div>
              <span>{config.icon}</span>{' '}
              <strong>{notif.judul}</strong>
              <p style={{ margin: '4px 0 0', fontSize: '0.9rem', color: '#555' }}>
                {notif.pesan}
              </p>
            </div>
            <button 
              onClick={() => tandaiDibaca(notif.id)}
              style={{
                background: 'none',
                border: '1px solid #ccc',
                borderRadius: '4px',
                padding: '4px 8px',
                cursor: 'pointer',
                fontSize: '0.8rem'
              }}
            >
              ✓ Dibaca
            </button>
          </div>
        );
      })}
    </div>
  );
}

// Penggunaan:
function App() {
  const notifikasi = [
    { id: 1, tipe: 'sukses', judul: 'Pembayaran Berhasil', pesan: 'Pesanan #123 sudah dibayar' },
    { id: 2, tipe: 'info', judul: 'Promo Baru', pesan: 'Diskon 30% untuk semua produk!' },
    { id: 3, tipe: 'warning', judul: 'Stok Menipis', pesan: 'Produk wishlist kamu tinggal 2' },
    { id: 4, tipe: 'error', judul: 'Gagal Upload', pesan: 'File terlalu besar, max 5MB' },
  ];
  
  return <PusatNotifikasi notifikasi={notifikasi} />;
}

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.