Bab 9: UI Kamu Sebagai Tree (Pohon)

7 menit baca

Kenapa Pohon?

Buka mata kamu dan lihat sekeliling. Banyak hal di dunia ini punya struktur pohon. Silsilah keluarga: kakek-nenek di atas, orang tua di tengah, anak-cucu di bawah. Struktur organisasi perusahaan: CEO di puncak, manajer di bawahnya, staff di bawah lagi. Folder di komputer kamu: drive C, di dalamnya ada folder Documents, di dalamnya ada folder Proyek, di dalamnya ada file-file.

React juga melihat UI kamu sebagai pohon (tree). Setiap komponen adalah "cabang" atau "daun" di pohon itu. Dan memahami struktur pohon ini penting banget untuk:

  1. Memahami bagaimana data mengalir di aplikasi
  2. Mengoptimalkan performa
  3. Debugging ketika ada masalah
  4. Mendesain arsitektur komponen yang baik

Apa Itu Component Tree?

Ketika kamu menulis komponen React yang memanggil komponen lain, secara otomatis terbentuk hubungan parent-child (induk-anak). Kumpulan hubungan ini membentuk pohon.

jsx
// Komponen-komponen
function App() {
  return (
    <div>
      <Header />
      <Main />
      <Footer />
    </div>
  );
}

function Header() {
  return (
    <header>
      <Logo />
      <Navigasi />
    </header>
  );
}

function Main() {
  return (
    <main>
      <Sidebar />
      <Konten />
    </main>
  );
}

function Konten() {
  return (
    <section>
      <Artikel />
      <Komentar />
    </section>
  );
}

Pohon yang terbentuk:

App ├── Header │ ├── Logo │ └── Navigasi ├── Main │ ├── Sidebar │ └── Konten │ ├── Artikel │ └── Komentar └── Footer
💡Info

Bayangin App itu kakek. Header, Main, Footer itu anak-anaknya. Logo dan Navigasi itu cucu dari jalur Header. Dan seterusnya.

Sama kayak pohon keluarga:

  • Setiap orang punya satu orang tua (parent) di pohon
  • Setiap orang bisa punya banyak anak (children)
  • Ada yang jadi "daun" (leaf) karena nggak punya anak lagi

Di React:

  • Setiap komponen punya satu parent yang me-render-nya
  • Setiap komponen bisa me-render banyak child
  • Komponen yang nggak me-render komponen lain disebut "leaf component"

Render Tree vs Module Dependency Tree

Ada dua jenis "pohon" yang perlu kamu pahami di React:

1. Render Tree (Pohon Render)

Render tree menggambarkan hubungan rendering antar komponen. Siapa me-render siapa.

jsx
// Render tree berubah berdasarkan KONDISI!
function App() {
  const [halaman, setHalaman] = useState('beranda');
  
  return (
    <div>
      <Navbar onNavigasi={setHalaman} />
      {halaman === 'beranda' && <Beranda />}
      {halaman === 'produk' && <HalamanProduk />}
      {halaman === 'profil' && <Profil />}
    </div>
  );
}

Render tree-nya dinamis. Kalau halaman = 'beranda':

App ├── Navbar └── Beranda ├── BannerPromo └── ProdukPopuler

Kalau halaman = 'produk':

App ├── Navbar └── HalamanProduk ├── FilterProduk └── GridProduk ├── KartuProduk ├── KartuProduk └── KartuProduk

Perhatiin: pohonnya berubah tergantung state! Komponen yang nggak di-render nggak ada di pohon.

2. Module Dependency Tree (Pohon Dependensi Modul)

Module dependency tree menggambarkan hubungan import antar file. File mana yang butuh file mana.

jsx
// App.jsx
import { Header } from './Header';
import { Main } from './Main';
import { Footer } from './Footer';

// Header.jsx
import { Logo } from './Logo';
import { Navigasi } from './Navigasi';
import { useAuth } from '../hooks/useAuth';

// Main.jsx
import { Sidebar } from './Sidebar';
import { Konten } from './Konten';
import { useData } from '../hooks/useData';

// Konten.jsx
import { Artikel } from './Artikel';
import { Komentar } from './Komentar';
import { formatTanggal } from '../utils/format';

Module dependency tree:

App.jsx ├── Header.jsx │ ├── Logo.jsx │ ├── Navigasi.jsx │ └── hooks/useAuth.js ├── Main.jsx │ ├── Sidebar.jsx │ ├── Konten.jsx │ │ ├── Artikel.jsx │ │ ├── Komentar.jsx │ │ └── utils/format.js │ └── hooks/useData.js └── Footer.jsx

Perbedaan utama:

  • Render tree cuma berisi komponen dan berubah saat runtime
  • Module dependency tree berisi semua file (komponen, hooks, utils, styles) dan fixed saat build time
  • Bundler (Webpack, Vite) pakai module dependency tree untuk menentukan file apa yang perlu di-bundle

Kenapa Module Tree Penting?

Module dependency tree menentukan bundle size aplikasi kamu. Kalau satu file import library besar, semua file yang bergantung padanya juga "membawa" library itu.

jsx
// ❌ Import besar di komponen kecil
// Kalau Artikel.jsx import library charting 500KB,
// semua halaman yang render Artikel juga "membawa" 500KB itu

// ✅ Code splitting - import dinamis
// Pisahkan komponen berat ke chunk terpisah
const GrafikBerat = lazy(() => import('./GrafikBerat'));

Bagaimana React Melintasi (Traverse) Pohon

Ketika React me-render aplikasi kamu, dia melintasi pohon dari atas ke bawah (top-down):

  1. Mulai dari root (biasanya <App />)
  2. Render App, lihat apa yang di-return
  3. Untuk setiap child component yang ditemukan, render juga
  4. Terus ke bawah sampai ketemu "daun" (elemen HTML biasa atau komponen tanpa child)
jsx
// React melintasi pohon ini:
function App() {           // 1. React render App
  return (
    <Layout>               // 2. React render Layout
      <Header />           // 3. React render Header
      <Konten>             // 4. React render Konten
        <Artikel />        // 5. React render Artikel
      </Konten>
    </Layout>
  );
}

// Urutan render: App → Layout → Header → Konten → Artikel

Re-render: Nggak Selalu Seluruh Pohon

Ketika state berubah, React nggak selalu me-render ulang SELURUH pohon. Dia cuma me-render ulang:

  1. Komponen yang state-nya berubah
  2. Semua child/descendant dari komponen itu
jsx
function App() {
  return (
    <div>
      <Header />           {/* Nggak re-render kalau state di Konten berubah */}
      <Konten />           {/* Re-render kalau state di sini berubah */}
      <Footer />           {/* Nggak re-render kalau state di Konten berubah */}
    </div>
  );
}

function Konten() {
  const [count, setCount] = useState(0);  // State berubah di sini
  
  return (
    <div>
      <p>{count}</p>
      <Artikel />          {/* Re-render (child dari Konten) */}
      <Komentar />         {/* Re-render (child dari Konten) */}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

Ini kenapa posisi state di pohon itu penting. State yang ditaruh terlalu tinggi (di App misalnya) akan menyebabkan banyak komponen re-render yang nggak perlu.

Top-Down Data Flow (Aliran Data dari Atas ke Bawah)

Di React, data mengalir satu arah: dari atas ke bawah. Parent mengirim data ke child lewat props. Child nggak bisa "mengirim" data ke parent secara langsung.

App (punya data user) │ ├── Header (terima user.nama via props) │ └── Avatar (terima user.foto via props) │ ├── Main (terima user.pesanan via props) │ ├── DaftarPesanan (terima pesanan via props) │ │ └── ItemPesanan (terima satu pesanan via props) │ └── RingkasanBelanja (terima total via props) │ └── Footer (nggak butuh data user)
💡Info

Data di React itu kayak air terjun. Air (data) mengalir dari atas (parent) ke bawah (child). Air nggak bisa naik ke atas dengan sendirinya.

Kalau child mau "ngomong" ke parent (misalnya user klik tombol di child), dia pakai callback function yang dikirim parent lewat props:

jsx
function Parent() {
  const [pesan, setPesan] = useState('');
  
  // Parent kirim fungsi ke child
  return <Child onKirimPesan={(p) => setPesan(p)} />;
}

function Child({ onKirimPesan }) {
  // Child panggil fungsi dari parent
  return <button onClick={() => onKirimPesan('Halo!')}>Kirim</button>;
}

Data tetap "mengalir ke bawah" (fungsi dikirim dari parent ke child). Tapi efeknya bisa "naik ke atas" (child memanggil fungsi yang mengubah state parent).

Kenapa Struktur Pohon Penting untuk Performa?

1. Re-render Cascade

Ketika state berubah di satu komponen, semua descendant-nya di-render ulang. Kalau pohon kamu "gemuk" (banyak child di setiap level), satu perubahan state bisa trigger ratusan re-render.

// Pohon "gemuk" - state di App trigger re-render SEMUA App (state di sini) ├── Komponen1 │ ├── SubKomponen1a │ ├── SubKomponen1b │ └── SubKomponen1c ├── Komponen2 │ ├── SubKomponen2a │ └── SubKomponen2b ├── Komponen3 │ ├── SubKomponen3a │ ├── SubKomponen3b │ └── SubKomponen3c └── Komponen4 └── SubKomponen4a

Solusi: Taruh state sedekat mungkin dengan komponen yang membutuhkannya.

jsx
// ❌ State terlalu tinggi - semua re-render
function App() {
  const [searchQuery, setSearchQuery] = useState('');
  
  return (
    <div>
      <Header />                    {/* Re-render padahal nggak butuh searchQuery */}
      <Sidebar />                   {/* Re-render padahal nggak butuh searchQuery */}
      <SearchBar query={searchQuery} onChange={setSearchQuery} />
      <SearchResults query={searchQuery} />
      <Footer />                    {/* Re-render padahal nggak butuh searchQuery */}
    </div>
  );
}

// ✅ State di tempat yang tepat - cuma yang butuh yang re-render
function App() {
  return (
    <div>
      <Header />
      <Sidebar />
      <SearchSection />             {/* State di dalam sini */}
      <Footer />
    </div>
  );
}

function SearchSection() {
  const [searchQuery, setSearchQuery] = useState('');
  
  return (
    <div>
      <SearchBar query={searchQuery} onChange={setSearchQuery} />
      <SearchResults query={searchQuery} />
    </div>
  );
}

2. Component Mounting/Unmounting

Ketika posisi komponen di pohon berubah, React bisa memutuskan untuk unmount (hapus) dan mount (buat baru) komponen. Ini berarti semua state internal komponen itu hilang.

jsx
function App() {
  const [isPremium, setIsPremium] = useState(false);
  
  return (
    <div>
      {isPremium 
        ? <PremiumChat />    // Posisi: child pertama dari div
        : <FreeChat />       // Posisi: child pertama dari div
      }
    </div>
  );
}

// Ketika isPremium berubah dari false ke true:
// 1. FreeChat di-unmount (state-nya hilang!)
// 2. PremiumChat di-mount (mulai dari awal)

Tapi kalau komponen yang SAMA di posisi yang SAMA, state-nya dipertahankan:

jsx
function App() {
  const [isPremium, setIsPremium] = useState(false);
  
  return (
    <div>
      {/* Komponen SAMA (Chat) di posisi SAMA - state dipertahankan! */}
      <Chat isPremium={isPremium} />
    </div>
  );
}
// Ketika isPremium berubah, Chat NGGAK di-unmount
// State internal Chat (pesan yang sudah diketik, dll) tetap ada

3. Key Mengontrol Identity di Pohon

Ingat key dari bab rendering list? Key juga bisa dipakai untuk "memaksa" React menganggap komponen sebagai instance baru:

jsx
function ChatApp() {
  const [kontakAktif, setKontakAktif] = useState(kontak[0]);
  
  return (
    <div>
      <DaftarKontak onPilih={setKontakAktif} />
      {/* Key berubah = React unmount yang lama, mount yang baru */}
      <ChatPanel key={kontakAktif.id} kontak={kontakAktif} />
    </div>
  );
}

// Tanpa key: pindah kontak, tapi pesan draft masih ada (bug!)
// Dengan key: pindah kontak, chat panel fresh (benar!)

Visualisasi Pohon: Contoh Aplikasi E-Commerce

Mari kita lihat pohon dari aplikasi e-commerce sederhana:

jsx
// Struktur komponen
function App() {
  const [user, setUser] = useState(null);
  const [keranjang, setKeranjang] = useState([]);
  
  return (
    <div className="app">
      <Navbar user={user} jumlahKeranjang={keranjang.length} />
      <Routes>
        <Route path="/" element={<Beranda />} />
        <Route path="/produk" element={<HalamanProduk />} />
        <Route path="/produk/:id" element={<DetailProduk onTambahKeranjang={...} />} />
        <Route path="/keranjang" element={<Keranjang items={keranjang} />} />
        <Route path="/checkout" element={<Checkout user={user} items={keranjang} />} />
      </Routes>
      <Footer />
    </div>
  );
}

Pohon saat user di halaman produk:

App ├── Navbar │ ├── Logo │ ├── SearchBar │ ├── NavLinks │ └── KeranjangIcon (badge: 3) ├── HalamanProduk │ ├── FilterSidebar │ │ ├── FilterKategori │ │ ├── FilterHarga │ │ └── FilterRating │ └── GridProduk │ ├── KartuProduk (Sepatu A) │ ├── KartuProduk (Tas B) │ ├── KartuProduk (Baju C) │ └── KartuProduk (Celana D) └── Footer ├── LinkSosmed └── Copyright

Pohon saat user di halaman checkout:

App ├── Navbar │ ├── Logo │ ├── SearchBar │ ├── NavLinks │ └── KeranjangIcon (badge: 3) ├── Checkout │ ├── StepIndicator │ ├── FormAlamat │ │ ├── InputNama │ │ ├── InputAlamat │ │ ├── InputKota │ │ └── InputKodePos │ ├── RingkasanPesanan │ │ ├── ItemPesanan (Sepatu A) │ │ ├── ItemPesanan (Tas B) │ │ └── TotalHarga │ └── TombolBayar └── Footer ├── LinkSosmed └── Copyright

Perhatiin: Navbar dan Footer tetap ada di kedua pohon. Yang berubah cuma bagian tengah (konten utama). Ini pattern umum di aplikasi web.

Prinsip Desain Pohon yang Baik

1. Flat > Deep (Dangkal Lebih Baik dari Dalam)

// ❌ Terlalu dalam - susah di-debug, props drilling App └── Layout └── PageWrapper └── ContentArea └── Section └── Card └── CardBody └── Text ← 7 level deep! // ✅ Lebih flat - mudah dipahami App ├── Layout ├── Card │ └── CardBody └── Text ← 3 level max

2. Komponen Leaf Harus Kecil dan Focused

"Daun" di pohon (komponen yang nggak punya child komponen) sebaiknya kecil dan punya satu tanggung jawab:

jsx
// ✅ Leaf components yang bagus - kecil, focused
function Avatar({ src, alt, ukuran }) {
  return <img src={src} alt={alt} style={{ width: ukuran, height: ukuran, borderRadius: '50%' }} />;
}

function Badge({ teks, warna }) {
  return <span style={{ backgroundColor: warna, padding: '2px 6px' }}>{teks}</span>;
}

function Harga({ jumlah, coret }) {
  return (
    <span style={{ textDecoration: coret ? 'line-through' : 'none' }}>
      Rp {jumlah.toLocaleString('id-ID')}
    </span>
  );
}

3. State Ownership yang Jelas

Setiap state harus "dimiliki" oleh komponen yang tepat di pohon:

jsx
// Tanya: "Siapa yang BUTUH state ini?"
// Jawab: "Taruh state di ANCESTOR TERDEKAT dari semua komponen yang butuh"

// Contoh: filter dan hasil pencarian butuh query yang sama
// → Taruh state di parent keduanya

function SearchSection() {
  const [query, setQuery] = useState('');  // State di parent keduanya
  
  return (
    <>
      <SearchInput value={query} onChange={setQuery} />  {/* Butuh query */}
      <SearchResults query={query} />                     {/* Butuh query */}
    </>
  );
}

Contoh Lengkap: Memahami Pohon Lewat Kode

jsx
// ============================================
// APLIKASI TODO - Analisis Struktur Pohon
// ============================================

// Level 0: Root
function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('semua');
  
  // Derived state (dihitung dari state, bukan state sendiri)
  const filteredTodos = todos.filter(todo => {
    if (filter === 'aktif') return !todo.selesai;
    if (filter === 'selesai') return todo.selesai;
    return true;
  });
  
  const jumlahAktif = todos.filter(t => !t.selesai).length;
  
  function handleTambah(teks) {
    setTodos([...todos, { id: Date.now(), teks, selesai: false }]);
  }
  
  function handleToggle(id) {
    setTodos(todos.map(t => 
      t.id === id ? { ...t, selesai: !t.selesai } : t
    ));
  }
  
  function handleHapus(id) {
    setTodos(todos.filter(t => t.id !== id));
  }
  
  return (
    <div className="todo-app">
      {/* Level 1 */}
      <HeaderTodo jumlahAktif={jumlahAktif} />
      <FormTambah onTambah={handleTambah} />
      <FilterBar filter={filter} onUbahFilter={setFilter} />
      <DaftarTodo 
        todos={filteredTodos} 
        onToggle={handleToggle}
        onHapus={handleHapus}
      />
      <FooterTodo total={todos.length} aktif={jumlahAktif} />
    </div>
  );
}

// Level 1: Header
function HeaderTodo({ jumlahAktif }) {
  return (
    <header>
      <h1>📝 Todo App</h1>
      {jumlahAktif > 0 && (
        <p>{jumlahAktif} tugas belum selesai</p>
      )}
    </header>
  );
}

// Level 1: Form (punya state internal sendiri)
function FormTambah({ onTambah }) {
  const [input, setInput] = useState('');  // State lokal - nggak perlu di parent
  
  function handleSubmit(e) {
    e.preventDefault();
    if (input.trim()) {
      onTambah(input.trim());
      setInput('');
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Tambah tugas baru..."
      />
      <button type="submit">Tambah</button>
    </form>
  );
}

// Level 1: Filter
function FilterBar({ filter, onUbahFilter }) {
  const opsi = ['semua', 'aktif', 'selesai'];
  
  return (
    <div className="filter-bar">
      {opsi.map(o => (
        <button 
          key={o}
          onClick={() => onUbahFilter(o)}
          className={filter === o ? 'aktif' : ''}
        >
          {o.charAt(0).toUpperCase() + o.slice(1)}
        </button>
      ))}
    </div>
  );
}

// Level 1: Daftar
function DaftarTodo({ todos, onToggle, onHapus }) {
  if (todos.length === 0) {
    return <p className="empty">Tidak ada tugas.</p>;
  }
  
  return (
    <ul className="daftar-todo">
      {todos.map(todo => (
        // Level 2: Item individual
        <ItemTodo 
          key={todo.id}
          todo={todo}
          onToggle={() => onToggle(todo.id)}
          onHapus={() => onHapus(todo.id)}
        />
      ))}
    </ul>
  );
}

// Level 2: Satu item todo (leaf component)
function ItemTodo({ todo, onToggle, onHapus }) {
  return (
    <li className={todo.selesai ? 'selesai' : ''}>
      <input 
        type="checkbox" 
        checked={todo.selesai}
        onChange={onToggle}
      />
      <span>{todo.teks}</span>
      <button onClick={onHapus}>🗑️</button>
    </li>
  );
}

// Level 1: Footer
function FooterTodo({ total, aktif }) {
  return (
    <footer>
      <p>{total} total tugas | {aktif} aktif | {total - aktif} selesai</p>
    </footer>
  );
}

Pohon dari aplikasi di atas:

TodoApp (state: todos, filter) ├── HeaderTodo (props: jumlahAktif) ├── FormTambah (state lokal: input) (props: onTambah) ├── FilterBar (props: filter, onUbahFilter) ├── DaftarTodo (props: todos, onToggle, onHapus) │ ├── ItemTodo (props: todo, onToggle, onHapus) │ ├── ItemTodo │ └── ItemTodo └── FooterTodo (props: total, aktif)

Perhatikan:

  • State utama (todos, filter) ada di TodoApp karena banyak child yang butuh
  • FormTambah punya state lokal (input) karena cuma dia yang butuh
  • Data mengalir ke bawah lewat props
  • Aksi mengalir ke atas lewat callback (onTambah, onToggle, onHapus)

React DevTools: Melihat Pohon Secara Visual

React DevTools (extension browser) punya tab "Components" yang menampilkan pohon komponen secara visual. Ini sangat berguna untuk:

  1. Melihat hierarki komponen di aplikasi
  2. Inspeksi props dan state setiap komponen
  3. Melihat re-render (highlight komponen yang re-render)
  4. Profiling performa (komponen mana yang lambat)
// Di React DevTools, kamu bisa lihat: // - Pohon komponen (mirip DOM inspector tapi untuk React) // - Props yang diterima setiap komponen // - State internal setiap komponen // - Context yang dipakai // - Hooks yang digunakan

Tips pakai DevTools:

  • Install "React Developer Tools" di Chrome/Firefox
  • Buka tab "Components" di DevTools
  • Klik komponen untuk lihat props/state-nya
  • Aktifkan "Highlight updates" untuk lihat re-render

⚠️ Jebakan

Jebakan 1: Props Drilling yang Terlalu Dalam

jsx
// ❌ Props dioper 5 level tanpa dipakai di tengah
function App() {
  const tema = 'dark';
  return <Layout tema={tema} />;
}
function Layout({ tema }) {
  return <Main tema={tema} />;        // Cuma forward
}
function Main({ tema }) {
  return <Konten tema={tema} />;      // Cuma forward
}
function Konten({ tema }) {
  return <Artikel tema={tema} />;     // Cuma forward
}
function Artikel({ tema }) {
  return <p className={tema}>...</p>; // Baru dipakai di sini!
}

// ✅ Solusi: Context API (dibahas di bab lain)
// Atau: komposisi dengan children
function App() {
  const tema = 'dark';
  return (
    <Layout>
      <Main>
        <Konten>
          <Artikel tema={tema} />  {/* Langsung dari App ke Artikel */}
        </Konten>
      </Main>
    </Layout>
  );
}

Jebakan 2: State di Tempat yang Salah

jsx
// ❌ State terlalu tinggi - semua re-render saat input berubah
function App() {
  const [searchInput, setSearchInput] = useState('');  // Setiap keystroke re-render SEMUA
  
  return (
    <div>
      <Header />                    {/* Re-render nggak perlu */}
      <Sidebar />                   {/* Re-render nggak perlu */}
      <input value={searchInput} onChange={e => setSearchInput(e.target.value)} />
      <SearchResults query={searchInput} />
      <Footer />                    {/* Re-render nggak perlu */}
    </div>
  );
}

// ✅ State di tempat yang tepat
function App() {
  return (
    <div>
      <Header />
      <Sidebar />
      <SearchWidget />              {/* State di dalam sini */}
      <Footer />
    </div>
  );
}

function SearchWidget() {
  const [searchInput, setSearchInput] = useState('');
  return (
    <>
      <input value={searchInput} onChange={e => setSearchInput(e.target.value)} />
      <SearchResults query={searchInput} />
    </>
  );
}

Jebakan 3: Nggak Paham Kapan Komponen Unmount

jsx
// Komponen di posisi berbeda = instance berbeda = state hilang!
function App() {
  const [isAdmin, setIsAdmin] = useState(false);
  
  // ❌ Dua komponen berbeda di posisi yang sama
  // Ketika toggle, Chat di-unmount dan mount ulang (pesan hilang!)
  if (isAdmin) {
    return <Chat nama="Admin" />;
  }
  return <Chat nama="User" />;
  
  // ✅ Kalau mau pertahankan state, pakai komponen yang SAMA
  // dengan props berbeda (tanpa conditional rendering)
  return <Chat nama={isAdmin ? "Admin" : "User"} />;
}

Jebakan 4: Pohon Terlalu Flat (Komponen Terlalu Besar)

jsx
// ❌ Satu komponen ngerjain semuanya - susah di-maintain
function HalamanProduk() {
  // 200 baris kode...
  // State untuk filter, sort, pagination, modal, toast...
  // Render header, sidebar, grid, modal, toast, footer...
  return (
    <div>
      {/* 100 baris JSX */}
    </div>
  );
}

// ✅ Pecah jadi pohon yang terstruktur
function HalamanProduk() {
  return (
    <div>
      <HeaderProduk />
      <div className="layout">
        <FilterSidebar />
        <GridProduk />
      </div>
      <Pagination />
    </div>
  );
}

Ringkasan

KonsepPenjelasan
Component TreeHierarki parent-child dari komponen React
Render TreePohon yang terbentuk saat runtime (bisa berubah)
Module Dependency TreePohon import antar file (fixed saat build)
Top-down data flowData mengalir dari parent ke child lewat props
State placementTaruh state di ancestor terdekat yang membutuhkan
Mount/UnmountKomponen dibuat/dihapus berdasarkan posisi di pohon
KeyMengontrol identitas komponen di pohon

🏋️ Challenge

Challenge 1: Gambar Pohon Komponen

Perhatikan kode berikut. Gambarkan (tulis dalam format teks) pohon komponen yang terbentuk ketika tab = "pesanan":

jsx
function DashboardToko() {
  const [tab, setTab] = useState('pesanan');
  
  return (
    <div>
      <NavbarToko />
      <TabMenu tab={tab} onPilih={setTab} />
      {tab === 'pesanan' && <HalamanPesanan />}
      {tab === 'produk' && <HalamanProduk />}
      {tab === 'statistik' && <HalamanStatistik />}
      <FooterToko />
    </div>
  );
}

function HalamanPesanan() {
  return (
    <div>
      <FilterPesanan />
      <TabelPesanan />
      <PaginasiPesanan />
    </div>
  );
}

function TabelPesanan() {
  const pesanan = [/* ... */];
  return (
    <table>
      {pesanan.map(p => <BarisPesanan key={p.id} data={p} />)}
    </table>
  );
}
💡 Hint
  • Mulai dari root (DashboardToko)
  • Hanya komponen yang di-render yang masuk pohon
  • tab === 'pesanan' berarti HalamanPesanan yang di-render
  • TabelPesanan punya child BarisPesanan (dari map)
✅ Solusi
DashboardToko (state: tab = "pesanan") ├── NavbarToko ├── TabMenu (props: tab, onPilih) ├── HalamanPesanan │ ├── FilterPesanan │ ├── TabelPesanan │ │ ├── BarisPesanan (key: id-1) │ │ ├── BarisPesanan (key: id-2) │ │ ├── BarisPesanan (key: id-3) │ │ └── ... (sesuai jumlah pesanan) │ └── PaginasiPesanan └── FooterToko Catatan: - HalamanProduk dan HalamanStatistik TIDAK ada di pohon karena tab !== 'produk' dan tab !== 'statistik' - Kalau tab berubah ke 'produk': - HalamanPesanan di-unmount (hilang dari pohon) - HalamanProduk di-mount (muncul di pohon) - NavbarToko, TabMenu, FooterToko tetap ada

Challenge 2: Optimasi Penempatan State

Kode berikut punya masalah performa karena state di tempat yang salah. Refactor supaya lebih optimal:

jsx
function App() {
  const [namaUser, setNamaUser] = useState('Budi');
  const [darkMode, setDarkMode] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const [notifCount, setNotifCount] = useState(5);
  
  return (
    <div className={darkMode ? 'dark' : 'light'}>
      <Navbar 
        nama={namaUser} 
        notif={notifCount}
        darkMode={darkMode}
        onToggleDark={() => setDarkMode(!darkMode)}
      />
      <SearchSection 
        query={searchQuery}
        onChange={setSearchQuery}
      />
      <KontenUtama nama={namaUser} />
      <Sidebar />
      <Footer />
    </div>
  );
}

Masalah: setiap kali user mengetik di search (searchQuery berubah), SEMUA komponen re-render termasuk Navbar, KontenUtama, Sidebar, dan Footer.

💡 Hint
  • searchQuery cuma dipakai oleh SearchSection
  • Pindahkan state yang cuma dipakai satu komponen ke dalam komponen itu
  • darkMode mempengaruhi className di root div, jadi harus tetap di App
  • namaUser dipakai di Navbar dan KontenUtama, jadi tetap di App
✅ Solusi
jsx
// ✅ State dipindah ke tempat yang tepat
function App() {
  const [namaUser, setNamaUser] = useState('Budi');
  const [darkMode, setDarkMode] = useState(false);
  const [notifCount, setNotifCount] = useState(5);
  // searchQuery DIHAPUS dari sini!
  
  return (
    <div className={darkMode ? 'dark' : 'light'}>
      <Navbar 
        nama={namaUser} 
        notif={notifCount}
        darkMode={darkMode}
        onToggleDark={() => setDarkMode(!darkMode)}
      />
      {/* SearchSection sekarang kelola state sendiri */}
      <SearchSection />
      <KontenUtama nama={namaUser} />
      <Sidebar />
      <Footer />
    </div>
  );
}

// searchQuery dipindah ke dalam SearchSection
function SearchSection() {
  // State lokal - perubahan di sini NGGAK trigger re-render App!
  const [searchQuery, setSearchQuery] = useState('');
  
  return (
    <div className="search-section">
      <input 
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="Cari..."
      />
      <SearchResults query={searchQuery} />
    </div>
  );
}

/*
Sebelum refactor:
- User ketik di search → App re-render → SEMUA child re-render
- Navbar, KontenUtama, Sidebar, Footer re-render padahal nggak berubah

Sesudah refactor:
- User ketik di search → SearchSection re-render → cuma SearchResults re-render
- Navbar, KontenUtama, Sidebar, Footer NGGAK re-render

Pohon sebelum:
App (state: namaUser, darkMode, searchQuery, notifCount)
├── Navbar
├── SearchSection (props: query, onChange)
├── KontenUtama
├── Sidebar
└── Footer

Pohon sesudah:
App (state: namaUser, darkMode, notifCount)
├── Navbar
├── SearchSection (state lokal: searchQuery)
│   └── SearchResults
├── KontenUtama
├── Sidebar
└── Footer
*/

Challenge 3: Desain Pohon Komponen

Kamu diminta membuat aplikasi "Warung Digital" dengan fitur:

  • Header dengan logo, search bar, dan ikon keranjang (dengan badge jumlah item)
  • Sidebar dengan kategori menu (Makanan, Minuman, Snack)
  • Grid produk yang bisa difilter berdasarkan kategori
  • Modal detail produk (muncul saat produk diklik)
  • Keranjang belanja (bisa buka/tutup)

Tugas: Desain pohon komponen yang optimal. Tentukan:

  1. Komponen apa saja yang dibutuhkan
  2. State apa yang dibutuhkan dan di mana penempatannya
  3. Props apa yang mengalir ke mana
💡 Hint
  • Pikirkan: state mana yang dipakai bersama oleh beberapa komponen?
  • keranjang dipakai oleh: badge di header, panel keranjang, dan tombol "tambah" di produk
  • kategoriAktif dipakai oleh: sidebar (highlight) dan grid (filter)
  • produkDipilih (untuk modal) cuma dipakai oleh modal itu sendiri
  • State yang cuma dipakai satu komponen → taruh di komponen itu
✅ Solusi
jsx
// ============================================
// DESAIN POHON: Warung Digital
// ============================================

/*
POHON KOMPONEN:

WarungApp (state: keranjang, kategoriAktif, keranjangBuka)
├── Header (props: jumlahKeranjang, onBukaKeranjang)
│   ├── Logo
│   ├── SearchBar (state lokal: query)
│   └── IkonKeranjang (props: jumlah, onClick)
├── LayoutUtama
│   ├── SidebarKategori (props: kategoriAktif, onPilihKategori)
│   │   └── ItemKategori (props: nama, aktif, onClick) [× N]
│   └── GridProduk (props: kategoriAktif, onTambahKeranjang)
│       └── KartuProduk (props: produk, onTambah, onKlikDetail) [× N]
├── ModalDetail (state lokal: produkDipilih) 
│   └── DetailProduk (props: produk, onTambahKeranjang)
├── PanelKeranjang (props: items, buka, onTutup, onHapusItem)
│   ├── ItemKeranjang (props: item, onHapus) [× N]
│   └── TotalBelanja (props: items)
└── Footer
*/

function WarungApp() {
  // State yang dipakai banyak komponen → di root
  const [keranjang, setKeranjang] = useState([]);
  const [kategoriAktif, setKategoriAktif] = useState('semua');
  const [keranjangBuka, setKeranjangBuka] = useState(false);
  
  function handleTambahKeranjang(produk) {
    setKeranjang([...keranjang, { ...produk, qty: 1 }]);
  }
  
  function handleHapusDariKeranjang(produkId) {
    setKeranjang(keranjang.filter(item => item.id !== produkId));
  }
  
  return (
    <div className="warung-app">
      <Header 
        jumlahKeranjang={keranjang.length}
        onBukaKeranjang={() => setKeranjangBuka(true)}
      />
      
      <div className="layout-utama">
        <SidebarKategori 
          kategoriAktif={kategoriAktif}
          onPilihKategori={setKategoriAktif}
        />
        <GridProduk 
          kategoriAktif={kategoriAktif}
          onTambahKeranjang={handleTambahKeranjang}
        />
      </div>
      
      {/* Panel keranjang - conditional render */}
      {keranjangBuka && (
        <PanelKeranjang 
          items={keranjang}
          onTutup={() => setKeranjangBuka(false)}
          onHapusItem={handleHapusDariKeranjang}
        />
      )}
      
      <Footer />
    </div>
  );
}

// SearchBar punya state lokal (query nggak perlu di parent)
function SearchBar() {
  const [query, setQuery] = useState('');
  // ... render input dan hasil search
}

// GridProduk bisa punya state lokal untuk modal
function GridProduk({ kategoriAktif, onTambahKeranjang }) {
  const [produkDipilih, setProdukDipilih] = useState(null);
  
  // Filter produk berdasarkan kategori
  const produkFiltered = semuaProduk.filter(p => 
    kategoriAktif === 'semua' || p.kategori === kategoriAktif
  );
  
  return (
    <div className="grid">
      {produkFiltered.map(produk => (
        <KartuProduk 
          key={produk.id}
          produk={produk}
          onTambah={() => onTambahKeranjang(produk)}
          onKlikDetail={() => setProdukDipilih(produk)}
        />
      ))}
      
      {/* Modal - state lokal karena cuma GridProduk yang butuh */}
      {produkDipilih && (
        <ModalDetail 
          produk={produkDipilih}
          onTutup={() => setProdukDipilih(null)}
          onTambahKeranjang={onTambahKeranjang}
        />
      )}
    </div>
  );
}

/*
PENJELASAN PENEMPATAN STATE:

1. keranjang → di WarungApp
   Alasan: dipakai oleh Header (badge), PanelKeranjang (list), 
   dan GridProduk (tombol tambah)

2. kategoriAktif → di WarungApp
   Alasan: dipakai oleh SidebarKategori (highlight) dan 
   GridProduk (filter)

3. keranjangBuka → di WarungApp
   Alasan: dipakai oleh Header (tombol buka) dan 
   PanelKeranjang (conditional render)

4. query (search) → di SearchBar (lokal)
   Alasan: cuma SearchBar yang butuh, nggak perlu di parent

5. produkDipilih (modal) → di GridProduk (lokal)
   Alasan: cuma GridProduk dan ModalDetail yang butuh,
   nggak perlu naik ke WarungApp
*/

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.