Dua fitur React kecil tapi sering dibutuhin: Fragment (return banyak elemen tanpa wrapper <div> yang bikin kotor DOM) dan Portal (render elemen ke DOM tree yang BEDA — escape parent boundary). Modal yang gak kepotong overflow, tooltip yang gak ke-clip, dropdown yang muncul di atas elemen lain — semua butuh Portal. Fragment buat list/group elemen tanpa membungkus dalam div tambahan yang gak perlu.
Fragment
// ❌ TANPA Fragment — wajib bungkus div, kotori DOM
function ListItem() {
return (
<div>
<td>Nama</td>
<td>Email</td>
</div>
)
}
// Hasilnya: <tr><div><td>...</td></div></tr> — INVALID HTML
// ✓ DENGAN Fragment — gak ada wrapper di DOM
function ListItem() {
return (
<>
<td>Nama</td>
<td>Email</td>
</>
)
}
// Hasilnya: <tr><td>...</td></tr> — bersih
<>...</> adalah shorthand untuk <React.Fragment>...</React.Fragment>.
Fragment dengan Key (untuk list)
items.map(item => (
<React.Fragment key={item.id}> {/* shorthand <>...</> tidak terima key */}
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</React.Fragment>
))
Pas perlu key di Fragment, pakai full syntax <React.Fragment key> (tidak bisa <key>...</>).
Portal — Render di Luar Parent
import { createPortal } from "react-dom"
function Modal({ children, onClose }) {
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body // render di body, bukan di parent
)
}
Komponen <Modal> di-call dari komponen mana pun, tapi dirender ke document.body — escape semua parent stacking context.
Kenapa Portal Penting
// MASALAH: parent punya overflow:hidden
<div style={{ overflow: "hidden", position: "relative" }}>
<Card>
<Tooltip /> {/* tooltip kepotong oleh parent overflow! */}
</Card>
</div>
// SOLUSI: Tooltip pakai Portal — render di body, gak kena clipping
function Tooltip() {
return createPortal(<div className="tooltip">...</div>, document.body)
}
Modal Lengkap dengan Portal
function Modal({ open, onClose, children }) {
useEffect(() => {
if (!open) return
const handleEscape = (e) => e.key === "Escape" && onClose()
document.addEventListener("keydown", handleEscape)
document.body.style.overflow = "hidden" // disable scroll background
return () => {
document.removeEventListener("keydown", handleEscape)
document.body.style.overflow = ""
}
}, [open, onClose])
if (!open) return null
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={e => e.stopPropagation()} role="dialog">
{children}
</div>
</div>,
document.body
)
}
💡 HTML5 punya
<dialog>native sekarang. Browser modern bisa pakai<dialog>element +dialog.showModal()— auto handle backdrop, ESC, focus trap. Pertimbangkan ini sebelum bikin custom modal portal.
Event Bubbling di Portal
Surprise: meski Portal render di body, event React tetap bubble lewat React component tree (bukan DOM tree). Click di Portal child = trigger handler di Portal parent (React tree), walaupun di DOM mereka di tempat berbeda.
function App() {
return (
<div onClick={() => console.log("App click")}>
<Modal>
<button>Klik</button> {/* klik trigger "App click" via React tree! */}
</Modal>
</div>
)
}
🎭 Analogi sehari-hari
Fragment itu kayak bungkus plastik kosong di supermarket — kamu pegang grup barang (h1, p, button), tapi pas dimasukkan ke kantong belanja gak ada plastik bungkus tambahan yang jadi sampah. Portal itu kayak walkie-talkie — komponen "ngomong" dari satu lantai gedung (parent DOM), pesan keluar di lantai berbeda (document.body). Berguna banget pas gedung-nya punya tembok tebal (overflow:hidden) atau ruangan yang sempit (z-index conflict). React tree (komunikasi event) tetap nyambung lewat walkie-talkie, tapi DOM (rendering fisik) di tempat lain.
⚠️ Jebakan yang sering ditemui
- Bungkus dalam
<div>instead of Fragment — DOM jadi banyak div sia-sia, breaks HTML semantics (<table>butuh<tr>direct child). <>...</>shorthand gak terima key/props — pakai<React.Fragment key={x}>full syntax.- Forget
e.stopPropagation()di modal content — klik di modal nutup modal (klik bubble ke overlay). - Forget restore body scroll di cleanup — modal close, scroll background tetep stuck. Wajib
document.body.style.overflow = ""di cleanup. - Lupa focus trap di modal — Tab keluar dari modal ke background (a11y bug). Pakai
<dialog>native atau library focus-trap. - Portal target gak ada —
createPortal(child, document.getElementById("missing"))= error. Pakaidocument.body(always exists) atau cek dulu. - Server-side rendering + Portal —
documentundefined di SSR. Conditional render:if (typeof window !== "undefined") createPortal(...).
🎯 Pakai Fragment atau Portal?
- Return banyak sibling elements → Fragment ✓ (jangan bungkus div sia-sia)
- Item dalam
<table>/<ul>yang butuh struktur ketat → Fragment ✓- Modal/Dialog → Portal ke body (atau pakai
<dialog>native)- Tooltip yang gak boleh kepotong overflow → Portal ✓
- Dropdown yang harus tampil di atas semua → Portal ✓
- Toast/Notification floating di pojok layar → Portal ke body
- Komponen yang tetap di parent normal → JANGAN portal (gak butuh)
Aturan: pakai Portal kalau elemen butuh "lepas" dari parent stacking context (overflow, z-index, transform). Selain itu, render normal saja.
TL;DR: Fragment (<>...</>) = group elements tanpa wrapper div — clean DOM, valid HTML. Pakai <React.Fragment key> kalau butuh key. Portal (createPortal(jsx, target)) = render JSX ke DOM tree berbeda (biasanya document.body) — escape parent overflow/z-index. Cocok buat modal, tooltip, dropdown floating. Event tetap bubble via React tree (bukan DOM tree). Modern alternatif modal: HTML5 <dialog> native — banyak handle gratis (backdrop, ESC, focus trap).