Bab 9: UI Kamu Sebagai Tree (Pohon)
⏱ 7 menit bacaKenapa 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:
- Memahami bagaimana data mengalir di aplikasi
- Mengoptimalkan performa
- Debugging ketika ada masalah
- 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.
// 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
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.
// 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.
// 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.
// ❌ 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):
- Mulai dari root (biasanya
<App />) - Render
App, lihat apa yang di-return - Untuk setiap child component yang ditemukan, render juga
- Terus ke bawah sampai ketemu "daun" (elemen HTML biasa atau komponen tanpa child)
// 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 → ArtikelRe-render: Nggak Selalu Seluruh Pohon
Ketika state berubah, React nggak selalu me-render ulang SELURUH pohon. Dia cuma me-render ulang:
- Komponen yang state-nya berubah
- Semua child/descendant dari komponen itu
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)
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:
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.
// ❌ 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.
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:
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 ada3. Key Mengontrol Identity di Pohon
Ingat key dari bab rendering list? Key juga bisa dipakai untuk "memaksa" React menganggap komponen sebagai instance baru:
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:
// 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:
// ✅ 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:
// 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
// ============================================
// 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 diTodoAppkarena banyak child yang butuh FormTambahpunya 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:
- Melihat hierarki komponen di aplikasi
- Inspeksi props dan state setiap komponen
- Melihat re-render (highlight komponen yang re-render)
- 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
// ❌ 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
// ❌ 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
// 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)
// ❌ 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
| Konsep | Penjelasan |
|---|---|
| Component Tree | Hierarki parent-child dari komponen React |
| Render Tree | Pohon yang terbentuk saat runtime (bisa berubah) |
| Module Dependency Tree | Pohon import antar file (fixed saat build) |
| Top-down data flow | Data mengalir dari parent ke child lewat props |
| State placement | Taruh state di ancestor terdekat yang membutuhkan |
| Mount/Unmount | Komponen dibuat/dihapus berdasarkan posisi di pohon |
| Key | Mengontrol 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":
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:
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
searchQuerycuma dipakai olehSearchSection- Pindahkan state yang cuma dipakai satu komponen ke dalam komponen itu
darkModemempengaruhi className di root div, jadi harus tetap di AppnamaUserdipakai di Navbar dan KontenUtama, jadi tetap di App
✅ Solusi
// ✅ 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:
- Komponen apa saja yang dibutuhkan
- State apa yang dibutuhkan dan di mana penempatannya
- Props apa yang mengalir ke mana
💡 Hint
- Pikirkan: state mana yang dipakai bersama oleh beberapa komponen?
keranjangdipakai oleh: badge di header, panel keranjang, dan tombol "tambah" di produkkategoriAktifdipakai 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
// ============================================
// 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.