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 naikkanz-indexmenunya 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>
<>dan</>adalah bentuk pendek dari<Fragment>dan</Fragment>(juga ditulis<React.Fragment>). React memakainya untuk mengelompokkan, lalu gak menaruh apa-apa di DOM.- Ini penting di tempat yang anaknya diatur ketat:
<dl>berisi<dt>dan<dd>,<tr>berisi<td>,<ul>berisi<li>. Layout flex dan grid juga cuma mengatur anak langsungnya, jadi<div>tambahan bisa merusak tata letak.
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>
- Isi yang dibuat dari
mapbutuhkey. Bentuk pendek<>gak bisa diberi key, jadi pakai bentuk panjang<Fragment key={item.term}>. Di project biasa,Fragmentdi-import dari"react". Di editor latihan sudah tersedia. {item.term}dan{item.desc}membaca isinya dari data, jadi daftarnya ikut berubah kalau datanya berubah.- Kalau tiap pasangan dibungkus
<div>,<dl>berisi dua<div>, bukan empatdt/dd.ddmilik Props juga gak lagi jadiddkedua di daftar itu, karena dia sendirian di dalam<div>-nya.
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>
)
}
createPortal(isi, nodeTujuan)darireact-dom: argumen pertama JSX yang mau dirender, argumen kedua node DOM tempat menaruhnya.- Di sini tujuannya
document.body, jadi<ul>menjadi anak<body>, bukan anak kartu, dan gak terpotong lagi. Banyak app menyiapkan<div id="modal-root">sendiri diindex.html, lalu memakaidocument.getElementById("modal-root"). - Di JSX, kamu tetap menulis
<Menu />di dalamKartuseperti biasa. Yang pindah cuma tempatnya di DOM. - Karena sudah keluar dari kartu, posisinya diatur sendiri, misalnya dari posisi tombolnya.
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
- Memberi key ke
<>. Bentuk pendek gak bisa menerima key. Di dalammap, tulis<Fragment key={...}>. - Menyisakan
<div>pembungkus. Kalau sudah pakai Fragment, hapus div-nya. Di latihan pertama, tulisan<divdi mana pun dalam kode membuatnya gagal. - Target portal belum ada.
createPortal(isi, document.getElementById("modal-root"))tanpa elemen#modal-rootdi halaman melempar errorTarget container is not a DOM element. - Klik di dalam portal sampai ke parent. Kalau parent menutup sesuatu waktu diklik, klik di dalam modal ikut memicunya. Hentikan dengan
e.stopPropagation()di dalam modal.
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.