Menu

Portal React: createPortal untuk Modal dan Tooltip

createPortal merender sebagian komponen ke node DOM lain, seperti document.body, sementara posisinya di tree React tetap sama. Pakai untuk modal, tooltip, dan menu yang harus lolos dari overflow hidden dan tumpukan z-index.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Portal merender sebagian komponen ke tempat lain di DOM, biasanya document.body, sementara posisinya di tree React tetap sama. Kamu membuatnya dengan createPortal(children, domNode) dari react-dom. Portal adalah cara modal, tooltip, dan menu dropdown lolos dari induk yang punya overflow: hidden atau stacking context sendiri.

Kotak di bawah memotong apa pun yang mencuat keluar darinya. Buka kedua tip.

Tip biasa terpotong di border putus-putus. Tip portal tampil utuh, karena node DOM-nya adalah anak dari <body>, bukan dari kotak itu. Hapus overflow: 'hidden' dari kotaknya, dan tip biasa tidak lagi terpotong.

Sintaksnya

import { createPortal } from 'react-dom';

createPortal(children, domNode, key?)
  • children adalah JSX apa pun: elemen, fragment, komponen.
  • domNode adalah elemen DOM yang sudah ada, seperti document.body atau document.getElementById('modal-root'). Elemen ini harus sudah ada saat portal dirender.
  • key bersifat opsional, untuk saat kamu merender list berisi portal.

createPortal mengembalikan sesuatu yang kamu taruh di JSX seperti elemen biasa. Ia tidak merender apa pun di posisi DOM milik induknya sendiri.

Modal adalah contoh klasiknya. Di dalam kartu yang punya transform, overlay position: fixed diposisikan relatif terhadap kartu itu, bukan terhadap jendela, lalu overflow: hidden milik kartu memotongnya. Jika dirender ke document.body, overlay itu menutupi seluruh viewport.

Buka modalnya, lalu tekan Escape atau klik overlay gelap untuk menutupnya. Fokus berpindah ke tombol Close saat modal terbuka. Sekarang kembalikan overlay tanpa createPortal (hapus pemanggilannya beserta argumen document.body): overlay mengecil seukuran kartu, karena transform menjadikan kartu itu containing block untuk elemen fixed.

Event mengalami bubbling lewat tree React

Portal mengubah tempat node DOM berada, bukan tempat komponen berada. Event React mengalami bubbling ke atas melalui tree React, jadi klik di dalam portal sampai ke onClick milik komponen yang merendernya, meskipun di DOM tombol itu adalah anak dari <body>.

Listener bawaan berbeda. Listener yang ditambahkan dengan addEventListener mengikuti tree DOM dan tidak pernah melihat klik itu.

Klik "Inside the wrapper": handler React maupun listener bawaan mencatat log. Klik "In a portal": hanya handler React yang mencatat. Tombol portal berada di luar wrapper putus-putus di layar, tetapi React tetap memperlakukannya sebagai anak.

Biasanya itulah yang kamu inginkan: onClick pada induk sebuah menu melihat klik pada item-item portal-nya, dan provider context di atas komponen itu juga berlaku di dalam portal. Hal ini bisa mengejutkan pada logika "klik di luar untuk menutup". onClick pada wrapper yang menutup menu juga terpicu oleh klik di dalam portal milik menu itu, jadi hentikan propagation di dalam menu (e.stopPropagation(), dibahas di halaman event), atau pakai listener document bawaan lalu periksa apakah target-nya berada di dalam node DOM portal.

Portal ke container milikmu sendiri

document.body adalah target yang paling sederhana. Beberapa aplikasi menambahkan container khusus ke index.html agar semua overlay berada di satu tempat dengan satu urutan tumpukan:

<body>
    <div id="root"></div>
    <div id="modal-root"></div>
</body>
createPortal(<Modal />, document.getElementById('modal-root'));

Dengan server rendering (Next.js dan framework lain), document tidak ada di server. Render portal hanya setelah komponen mount, misalnya di balik state mounted yang diatur ke true oleh sebuah effect.

Saat portal perlu diposisikan di samping pemicunya, ukur pemicu itu lebih dulu. Contoh pertama mengukurnya di handler klik; untuk tooltip yang tidak boleh berkedip, ukur di useLayoutEffect, yang berjalan sebelum browser menggambar.

Memindahkan markup ke document.body saja tidak membantu pengguna keyboard dan screen reader. Modal juga butuh:

  • role="dialog" dan aria-modal="true", dengan aria-labelledby yang menunjuk ke judulnya.
  • Fokus yang dipindahkan ke dalam dialog saat dibuka (contoh di atas memberi fokus ke Close), dan kembali ke tombol yang membukanya saat ditutup.
  • Escape untuk menutup.
  • Fokus yang tetap berada di dalam selama modal terbuka, agar Tab tidak berpindah ke halaman di belakangnya. Mengatur atribut inert pada root aplikasi selama modal terbuka memblokir fokus dan klik di sana.

Elemen bawaan <dialog> menangani sebagian besar hal ini untukmu. Jika dibuka dengan dialogRef.current.showModal(), elemen ini digambar di top layer browser di atas semua z-index, membuat bagian halaman lainnya inert, dan tertutup saat Escape ditekan. Elemen ini tidak butuh portal, jadi cocok sebagai pilihan default untuk dialog konfirmasi sederhana; portal tetap menjadi alat untuk tooltip, menu, dan overlay buatan sendiri.

Kenapa z-index saja tidak cukup

Developer sering beralih ke portal setelah z-index yang besar gagal menaruh menu di atas bagian halaman lainnya. Penyebabnya adalah stacking context. Elemen dengan position dan z-index, opacity di bawah 1, transform, filter, atau isolation: isolate memulai stacking context baru, dan nilai z-index anak-anaknya hanya bersaing satu sama lain di dalamnya. Anak dengan z-index: 9999 di dalam kartu dengan z-index: 1 tetap berada di bawah kartu sibling dengan z-index: 2.

Portal memindahkan elemen keluar dari semua ancestor semacam itu. Sebagai anak dari <body>, z-index-nya dibandingkan dengan elemen-elemen level teratas halaman, jadi nilai yang wajar seperti 1000 sudah cukup untuk overlay.

Kesalahan umum

Memasang portal ke node yang belum ada. document.getElementById('modal-root') mengembalikan null jika elemennya tidak ada, dan createPortal melempar "Target container is not a DOM element". Periksa HTML-nya, atau pasang portal ke document.body.

Membuat node target saat render. Menulis document.createElement('div') di isi komponen membuat node baru di setiap render. Buat sekali di dalam effect, atau pakai container yang tetap.

Popup yang tidak mengikuti pemicunya. Tooltip yang diposisikan dari getBoundingClientRect() tidak mengikuti pemicunya saat halaman digulir atau diubah ukurannya. Hitung ulang saat scroll dan resize (dan hapus listener itu di cleanup effect), atau tutup tooltip saat halaman digulir.

Pertanyaan yang sering diajukan

Apa itu portal di React?

Portal adalah cara merender anak ke node DOM di luar elemen DOM milik komponen induk. Kamu membuatnya dengan createPortal(children, domNode) dari react-dom. Anak-anaknya tetap berada di posisinya di tree React, jadi props, state, dan context berjalan seperti biasa.

Kapan sebaiknya saya memakai portal?

Saat sesuatu harus muncul di atas atau di luar container-nya: modal, tooltip, menu dropdown, toast. Induk dengan overflow: hidden, transform, atau stacking context sendiri akan memotong atau menyembunyikannya jika tidak memakai portal.

Apakah event mengalami bubbling keluar dari portal?

Ya, lewat tree React. Klik di dalam portal sampai ke handler onClick milik induk di React, meskipun node DOM-nya berada di document.body. Listener bawaan yang ditambahkan dengan addEventListener justru mengikuti tree DOM.

Apakah context berfungsi di dalam portal?

Ya. Context, seperti event, mengikuti tree React. Modal yang dirender ke document.body tetap membaca tema atau user dari provider di atas komponen yang membuatnya.

Apakah saya butuh portal untuk modal?

Tidak selalu. Elemen bawaan <dialog> yang dibuka dengan showModal() digambar di top layer browser, di atas semua z-index, dengan penanganan fokus bawaan. Portal adalah pilihan yang umum untuk modal buatan sendiri serta untuk tooltip dan menu.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai