Portal & Fragment — React

Dua fitur React kecil tapi sering dibutuhin: Fragment (return banyak elemen tanpa wrapper yang bikin kotor DOM) dan Portal (render elemen ke DOM tree yang BEDA

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

🎯 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).

Yang akan kamu pelajari