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…

Dua alat kecil ini mengatur di mana elemen berakhir di DOM. Fragment mengelompokkan beberapa elemen tanpa menambah elemen pembungkus. Portal merender elemen ke node DOM lain, di luar parent-nya, tapi di pohon React dia tetap anak komponen yang sama. Bayangin rak buku. Fragment itu kayak membawa dua buku sekaligus dengan tangan, tanpa memasukkannya ke kardus dulu: yang sampai di rak cuma dua buku itu. Portal itu kayak menitipkan satu buku di rak ruangan sebelah: bukunya ada di tempat lain, tapi di catatanmu dia tetap koleksimu.

🧪 Tebakan cepat: menu dropdown ada di dalam kartu yang punya overflow: hidden. Kamu naikkan z-index menunya jadi 9999. Apakah menunya masih terpotong? Tebak dulu, jawabannya ada di bagian "Portal: keluar dari kotak yang memotong".

Fragment: kelompok tanpa pembungkus

Sebuah komponen harus mengembalikan satu elemen. Mau mengembalikan <h1> dan <p> berdampingan? Bungkus dengan Fragment:

function App() {
  return (
    <>
      <h1>Judul</h1>
      <p>Paragraf</p>
    </>
  )
}
<!-- dibungkus <div>: ada elemen tambahan -->
<div><h1>Judul</h1><p>Paragraf</p></div>

<!-- dengan Fragment: cuma isinya -->
<h1>Judul</h1><p>Paragraf</p>

Fragment dengan key di dalam list

Daftar istilah <dl> butuh pasangan <dt> (istilah) dan <dd> (penjelasan) sebagai anak langsung. Tiap isi array menghasilkan dua elemen, jadi pasangannya dibungkus Fragment:

function Glossary({ items }) {
  return (
    <dl id="glossary">
      {items.map((item) => (
        <Fragment key={item.term}>
          <dt>{item.term}</dt>
          <dd>{item.desc}</dd>
        </Fragment>
      ))}
    </dl>
  )
}

function App() {
  const items = [
    { term: "JSX", desc: "Sintaks mirip HTML" },
    { term: "Props", desc: "Data dari parent" },
  ]
  return <Glossary items={items} />
}

Hasilnya di DOM, empat anak langsung tanpa pembungkus:

<dl id="glossary">
  <dt>JSX</dt><dd>Sintaks mirip HTML</dd>
  <dt>Props</dt><dd>Data dari parent</dd>
</dl>

Portal: keluar dari kotak yang memotong

overflow: hidden memotong semua yang ada di dalam kotak itu, termasuk menu position: absolute yang posisinya diukur dari kotak itu (kotaknya position: relative). z-index cuma mengatur urutan tumpukan, dia gak bisa membuat elemen keluar dari potongan parent-nya. Jadi menu dengan z-index: 9999 di dalam kartu ber-overflow: hidden tetap terpotong. Itu jawaban tebakannya.

Solusinya: taruh menunya di tempat lain di DOM, di luar kartu. Itu kerja createPortal:

import { createPortal } from "react-dom"

function Menu() {
  return createPortal(
    <ul className="menu">
      <li>Edit</li>
      <li>Bagikan</li>
      <li>Hapus</li>
    </ul>,
    document.body
  )
}

function Kartu() {
  return (
    <div className="kartu" style={{ position: "relative", overflow: "hidden", height: 110 }}>
      <h3>Laporan Mei</h3>
      <Menu />
    </div>
  )
}

Portal dipakai untuk elemen yang harus tampil di atas segalanya: modal, tooltip, dropdown, dan notifikasi (toast).

Event tetap mengikuti pohon React

Di DOM, menunya ada di <body>. Tapi di pohon React, Menu tetap anak Kartu. Event React naik (bubble) lewat pohon React, bukan pohon DOM:

<div className="kartu" onClick={() => console.log("Kartu onClick")}>
  <Menu />
</div>

Klik "Edit" di menu, dan console mencetak Kartu onClick, padahal di DOM menunya bukan di dalam kartu. Context juga tetap sampai ke dalam portal.

⚠️ Jebakan yang sering ketemu

TL;DR: Fragment (<>...</>) mengelompokkan beberapa elemen tanpa menambah elemen di DOM, jadi <dl>, <tr>, dan layout flex tetap rapi. Di dalam map, pakai <Fragment key={...}> karena <> gak bisa diberi key. Portal (createPortal(isi, nodeTujuan) dari react-dom) merender elemen ke node DOM lain, misalnya document.body, supaya lolos dari overflow: hidden parent-nya, yang gak bisa diatasi z-index berapa pun. Pakai untuk modal, tooltip, dan dropdown. Di pohon React elemennya tetap anak komponen yang sama, jadi event naik ke parent React-nya.

Yang akan kamu pelajari