Kenapa ini penting
Setelah aplikasi punya puluhan komponen, pertanyaannya bergeser dari "bagaimana membuat komponen" menjadi "bagaimana membagi tanggung jawab antar komponen". Tanpa pola yang jelas, satu komponen bisa berisi fetch data, logika filter, dan 200 baris JSX sekaligus. Komponen seperti itu sulit dites, sulit dipakai ulang, dan menakutkan untuk diubah. Pola komponen adalah resep yang sudah teruji oleh komunitas untuk memecah komponen. Di modul ini kita bahas tiga yang paling sering kamu temui: container/presentational, compound component, dan render props.
Konsep inti
1. Container / Presentational
Idenya memisahkan "apa datanya" dari "bagaimana tampilannya".
- Presentational component: hanya menerima props dan menampilkan UI. Tidak tahu data berasal dari mana. Mudah dipakai ulang dan mudah dicoba dengan data palsu.
- Container component: mengurus state, mengambil data, menangani event, lalu menyerahkan hasilnya ke presentational component.
Analogi restoran: koki di dapur (container) menyiapkan makanan, pelayan dan piring (presentational) menyajikannya. Piring yang sama bisa dipakai untuk menu apa pun.
Catatan: sejak ada hooks, peran "container" sering digantikan oleh custom hook (modul 7 dan 10). Tapi cara berpikirnya, memisahkan logika dan tampilan, tetap sangat berguna.
2. Compound component
Pola ini membuat sekumpulan komponen yang bekerja bersama dan berbagi state secara tersembunyi, seperti pasangan <select> dan <option> di HTML. Contoh: <Tabs>, <Tabs.List>, <Tabs.Tab>, <Tabs.Panel>. Pengguna komponen bebas menyusun urutan dan isi, tapi state tab aktif diurus oleh <Tabs> lewat Context (kita pakai Context versi sederhana di sini, detailnya di modul 6).
Kelebihannya dibanding satu komponen raksasa dengan props tabs={[...]}: lebih fleksibel. Kamu bisa menaruh ikon, badge, atau elemen lain di antara tab tanpa harus menambah props baru.
3. Render props
Render props adalah teknik di mana sebuah komponen menerima fungsi sebagai props (sering bernama render atau lewat children berbentuk fungsi). Komponen itu memanggil fungsi tersebut dengan data yang dia kelola, dan fungsi mengembalikan JSX. Jadi komponen menyediakan logika, pemanggil menentukan tampilan.
Pola ini populer sebelum hooks ada. Sekarang kebanyakan kasusnya lebih bersih diganti custom hook, tapi kamu tetap akan menemukannya di library (misalnya komponen list virtual atau form library), jadi kamu perlu bisa membacanya.
| Pola | Masalah yang diselesaikan | Alternatif modern |
|---|---|---|
| Container/Presentational | Logika dan tampilan bercampur | Custom hook + komponen tampilan |
| Compound component | Komponen kompleks dengan banyak bagian fleksibel | Tetap relevan (dipakai banyak UI library) |
| Render props | Berbagi logika tapi tampilan ditentukan pemanggil | Custom hook |
Bagaimana mengenali kapan perlu pola?
Gunakan tanda-tanda berikut sebagai alarm. Kalau satu komponen lebih dari sekitar 150-200 baris dan berisi fetch, state form, dan banyak JSX sekaligus, saatnya memisahkan logika dari tampilan. Kalau kamu menambah props ke-8 hanya untuk mengatur bagian kecil di dalam komponen (misalnya tabIcon, tabBadge, tabDisabled), pertimbangkan compound component. Kalau dua komponen berbeda punya logika sama persis tapi tampilannya berbeda, itu kandidat custom hook (atau render props di kode lama).
Pola juga punya biaya: lebih banyak file dan lapisan yang harus diikuti saat membaca kode. Karena itu jangan memulai dengan pola. Tulis versi paling sederhana dulu, lalu refactor ke pola ketika masalahnya benar-benar muncul.
Pola di library yang akan kamu temui
Kamu akan sering bertemu pola ini di dunia nyata. Library komponen seperti Radix UI dan Headless UI memakai compound component (<Dialog.Root>, <Dialog.Trigger>, <Dialog.Content>). Beberapa library form dan list virtual masih menyediakan API render props. Library data fetching seperti TanStack Query memakai custom hook (useQuery). Memahami ketiga pola di modul ini membuat dokumentasi library seperti itu jauh lebih mudah dibaca.
Contoh kode
Container/presentational:
// UserCard.jsx, presentational: cuma terima props
export function UserCard({ nama, email, online }) {
return (
<div className="user-card">
<strong>{nama}</strong> {online ? '🟢' : '⚪'}
<p>{email}</p>
</div>
);
}
// UserCardContainer.jsx, container: urus state & logika
import { useState } from 'react';
import { UserCard } from './UserCard.jsx';
export function UserCardContainer() {
const [user] = useState({ nama: 'Sari', email: 'sari@contoh.id' });
const [online, setOnline] = useState(false);
return (
<>
<UserCard nama={user.nama} email={user.email} online={online} />
<button onClick={() => setOnline((o) => !o)}>Toggle status</button>
</>
);
}
// UserCard bisa dipakai juga di halaman lain dengan data berbeda
Compound component Tabs:
// Tabs.jsx
import { createContext, useContext, useState } from 'react';
const TabsContext = createContext(null);
export function Tabs({ defaultValue, children }) {
const [aktif, setAktif] = useState(defaultValue);
// React 19: context bisa dirender langsung sebagai provider
return <TabsContext value={{ aktif, setAktif }}>{children}</TabsContext>;
}
function List({ children }) {
return <div role="tablist">{children}</div>;
}
function Tab({ value, children }) {
const { aktif, setAktif } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={aktif === value}
style={{ fontWeight: aktif === value ? 'bold' : 'normal' }}
onClick={() => setAktif(value)}
>
{children}
</button>
);
}
function Panel({ value, children }) {
const { aktif } = useContext(TabsContext);
return aktif === value ? <div role="tabpanel">{children}</div> : null;
}
// tempelkan sub-komponen sebagai property supaya bisa ditulis Tabs.Tab
Tabs.List = List;
Tabs.Tab = Tab;
Tabs.Panel = Panel;
// Pemakaian
import { Tabs } from './Tabs.jsx';
export default function Profil() {
return (
<Tabs defaultValue="info">
<Tabs.List>
<Tabs.Tab value="info">Info</Tabs.Tab>
<Tabs.Tab value="pesanan">Pesanan</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="info">Nama: Sari</Tabs.Panel>
<Tabs.Panel value="pesanan">Belum ada pesanan.</Tabs.Panel>
</Tabs>
);
}
// klik "Pesanan" → tampil "Belum ada pesanan."
Render props singkat:
// MousePosition.jsx, menyediakan logika posisi mouse
import { useState } from 'react';
export function MousePosition({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div
style={{ height: 200, border: '1px dashed gray' }}
onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}
>
{render(pos)}
</div>
);
}
// Pemanggil menentukan tampilan
<MousePosition render={({ x, y }) => <p>Mouse di ({x}, {y})</p>} />
// → "Mouse di (120, 85)" berubah saat mouse digerakkan
Kesalahan umum
- Presentational component yang diam-diam fetch data. Begitu ada
fetchdi dalamnya, komponen tidak lagi bisa dipakai ulang dengan data lain. Pindahkan ke container atau custom hook. - Memakai sub-komponen compound di luar parent-nya.
<Tabs.Tab>tanpa<Tabs>di atasnya akan membaca contextnulldan error. Beri pesan error jelas, misalnya cekif (!ctx) throw new Error('Tab harus di dalam Tabs'). - Memaksakan pola di komponen kecil. Komponen 15 baris tidak perlu dipecah jadi container dan presentational. Pola dipakai saat ada masalah nyata.
- Render props yang bersarang dalam-dalam. Tiga render props bertumpuk membuat kode seperti tangga. Ganti dengan custom hook.
- Lupa
keysaat compound component me-render list. Kalau tab dibuat dari array denganmap, setiapTabs.Tabtetap butuhkeyunik.
Latihan
- Mudah, Pecah komponen
ProductCardyang berisi state jumlah beli menjadiProductCardView(presentational) danProductCardContainer. - Sedang, Buat compound component
AccordiondenganAccordion.Item,Accordion.Header, danAccordion.Body, di mana hanya satu item terbuka pada satu waktu. - Menantang, Ubah
MousePositiondari render props menjadi custom hookuseMousePosition(), lalu bandingkan pemakaiannya.
Lihat solusi
// 1.
import { useState } from 'react';
function ProductCardView({ nama, jumlah, onTambah, onKurang }) {
return (
<div>
<h3>{nama}</h3>
<button onClick={onKurang}>-</button> {jumlah} <button onClick={onTambah}>+</button>
</div>
);
}
export function ProductCardContainer({ nama }) {
const [jumlah, setJumlah] = useState(0);
return (
<ProductCardView
nama={nama}
jumlah={jumlah}
onTambah={() => setJumlah((j) => j + 1)}
onKurang={() => setJumlah((j) => Math.max(0, j - 1))}
/>
);
}
// 2. Accordion.jsx
import { createContext, useContext, useState } from 'react';
const AccCtx = createContext(null);
const ItemCtx = createContext(null);
export function Accordion({ children }) {
const [terbuka, setTerbuka] = useState(null);
return <AccCtx value={{ terbuka, setTerbuka }}>{children}</AccCtx>;
}
function Item({ id, children }) {
return <ItemCtx value={id}>{children}</ItemCtx>;
}
function Header({ children }) {
const { terbuka, setTerbuka } = useContext(AccCtx);
const id = useContext(ItemCtx);
return (
<button onClick={() => setTerbuka(terbuka === id ? null : id)}>
{terbuka === id ? 'â–¾' : 'â–¸'} {children}
</button>
);
}
function Body({ children }) {
const { terbuka } = useContext(AccCtx);
const id = useContext(ItemCtx);
return terbuka === id ? <div>{children}</div> : null;
}
Accordion.Item = Item;
Accordion.Header = Header;
Accordion.Body = Body;
// 3. useMousePosition.js
import { useEffect, useState } from 'react';
export function useMousePosition() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return pos;
}
// Pemakaian: lebih datar, tanpa fungsi di dalam JSX
function Koordinat() {
const { x, y } = useMousePosition();
return <p>Mouse di ({x}, {y})</p>;
}
Ringkasan
- Container/presentational memisahkan pengurusan data dari tampilan.
- Presentational component hanya menerima props sehingga mudah dipakai ulang.
- Compound component adalah sekumpulan komponen yang berbagi state lewat Context, seperti
TabsdanTabs.Tab. - Render props menyerahkan fungsi yang menentukan tampilan dari data yang disediakan komponen.
- Di React modern, banyak kasus container dan render props lebih bersih diganti custom hook.