Portal renderuje część komponentu w innym miejscu DOM, zwykle w document.body, a komponent pozostaje w tym samym miejscu drzewa Reacta. Tworzysz go przez createPortal(children, domNode) z react-dom. Dzięki portalom modale, tooltipy i menu rozwijane wydostają się z rodzica, który ma overflow: hidden albo własny kontekst stosu.
Ramka poniżej przycina wszystko, co z niej wystaje. Otwórz obie podpowiedzi.
Zwykła podpowiedź jest ucięta na przerywanej ramce. Podpowiedź z portalu pojawia się w całości, bo jej węzeł DOM jest dzieckiem <body>, a nie ramki. Usuń overflow: 'hidden' z ramki, a zwykła podpowiedź przestanie być przycinana.
Składnia
import { createPortal } from 'react-dom';
createPortal(children, domNode, key?)
childrento dowolny JSX: element, fragment, komponent.domNodeto istniejący element DOM, na przykładdocument.bodyalbodocument.getElementById('modal-root'). Musi istnieć, gdy portal się renderuje.keyjest opcjonalny, na wypadek gdy renderujesz listę portali.
createPortal zwraca coś, co umieszczasz w JSX jak każdy element. W pozycji DOM samego rodzica nie renderuje niczego.
Modal w document.body
Modal to klasyczny przypadek. Wewnątrz karty z transform nakładka z position: fixed jest pozycjonowana względem tej karty zamiast okna, a overflow: hidden karty ją przycina. Wyrenderowana do document.body zakrywa cały obszar widoku.
Otwórz modal, a potem naciśnij Escape albo kliknij ciemną nakładkę, żeby go zamknąć. Po otwarciu fokus przechodzi na przycisk Close. Teraz zwróć nakładkę bez createPortal (usuń wywołanie i jego argument document.body): nakładka kurczy się do rozmiaru karty, bo transform sprawia, że karta staje się blokiem zawierającym dla elementów fixed.
Zdarzenia propagują się przez drzewo Reacta
Portal zmienia miejsce, w którym żyje węzeł DOM, a nie miejsce, w którym żyje komponent. Zdarzenia Reacta propagują się w górę drzewa Reacta, więc kliknięcie wewnątrz portalu dociera do onClick komponentu, który go wyrenderował, choć w DOM przycisk jest dzieckiem <body>.
Natywne nasłuchiwania działają inaczej. Nasłuchiwanie dodane przez addEventListener podąża za drzewem DOM i nigdy nie widzi tego kliknięcia.
Kliknij "Inside the wrapper": wypisują się i handler Reacta, i natywne nasłuchiwanie. Kliknij "In a portal": wypisuje się tylko handler Reacta. Przycisk z portalu leży na ekranie poza przerywaną ramką, a jednak React wciąż traktuje go jako dziecko.
Zwykle właśnie tego chcesz: onClick na rodzicu menu widzi kliknięcia w elementy jego portalu, a providery kontekstu nad komponentem działają też wewnątrz portalu. Może cię to jednak zaskoczyć przy logice „kliknij poza, żeby zamknąć". onClick na opakowaniu, które zamyka menu, uruchamia się też dla kliknięć wewnątrz portalu menu, więc zatrzymaj propagację wewnątrz menu (e.stopPropagation(), omówione na stronie o zdarzeniach) albo użyj natywnego nasłuchiwania na document i sprawdź, czy cel jest wewnątrz węzła DOM portalu.
Portale do własnego kontenera
document.body to najprostszy cel. Niektóre aplikacje dodają do index.html osobny kontener, żeby wszystkie nakładki miały jedno miejsce i jedną kolejność stosu:
<body>
<div id="root"></div>
<div id="modal-root"></div>
</body>
createPortal(<Modal />, document.getElementById('modal-root'));
Przy renderowaniu na serwerze (Next.js i inne frameworki) document nie istnieje na serwerze. Renderuj portal dopiero po zamontowaniu komponentu, na przykład za stanem mounted, który efekt ustawia na true.
Gdy portal trzeba ustawić obok elementu, który go otwiera, najpierw zmierz ten element. Pierwszy przykład mierzy go w handlerze kliknięcia; dla tooltipa, który nie może migotać, mierz w useLayoutEffect, który uruchamia się przed odmalowaniem ekranu przez przeglądarkę.
Dostępne modale
Samo przeniesienie znaczników do document.body nic nie daje użytkownikom klawiatury i czytników ekranu. Modal potrzebuje też:
role="dialog"iaria-modal="true", zaria-labelledbywskazującym na jego tytuł.- Przeniesienia fokusu do okna dialogowego przy otwarciu (przykład ustawia fokus na Close) i z powrotem na przycisk, który je otworzył, przy zamknięciu.
- Zamykania klawiszem Escape.
- Utrzymania fokusu w środku, dopóki jest otwarty, żeby Tab nie przechodził na stronę w tle. Ustawienie atrybutu
inertna roocie aplikacji, gdy modal jest otwarty, blokuje tam fokus i kliknięcia.
Natywny element <dialog> obsługuje większość tego za ciebie. Otwarty przez dialogRef.current.showModal() jest rysowany w górnej warstwie przeglądarki ponad każdym z-index, sprawia, że reszta strony staje się inert, i zamyka się klawiszem Escape. Nie potrzebuje portalu, więc to dobry domyślny wybór dla prostych okien potwierdzenia; portale pozostają narzędziem do tooltipów, menu i własnych nakładek.
Dlaczego sam z-index nie wystarcza
Programiści często sięgają po portal, gdy duży z-index nie umieszcza menu nad resztą strony. Powodem są konteksty stosu (stacking contexts). Element z position i z-index, opacity poniżej 1, transform, filter albo isolation: isolate tworzy nowy kontekst stosu, a wartości z-index jego dzieci konkurują tylko ze sobą w jego środku. Dziecko z z-index: 9999 wewnątrz karty z z-index: 1 nadal leży pod sąsiednią kartą z z-index: 2.
Portal wyjmuje element spod każdego takiego przodka. Jako dziecko <body> jego z-index jest porównywany z elementami najwyższego poziomu strony, więc dla nakładek wystarcza skromna wartość, taka jak 1000.
Częste błędy
Portal do węzła, który jeszcze nie istnieje. document.getElementById('modal-root') zwraca null, jeśli elementu brakuje, a createPortal rzuca "Target container is not a DOM element". Sprawdź HTML albo kieruj portal do document.body.
Tworzenie węzła docelowego podczas renderowania. Napisanie document.createElement('div') w ciele komponentu tworzy nowy węzeł przy każdym renderowaniu. Utwórz go raz w efekcie albo użyj stałego kontenera.
Wyskakujące okienka, które nie podążają za elementem otwierającym. Tooltip pozycjonowany na podstawie getBoundingClientRect() nie podąża za swoim elementem, gdy strona się przewija albo zmienia rozmiar. Przeliczaj pozycję przy scroll i resize (i usuwaj te nasłuchiwania w funkcji czyszczącej efektu) albo zamykaj tooltip, gdy strona się przewija.
Najczęściej zadawane pytania
Co to jest portal w React?
Sposób na wyrenderowanie dzieci do węzła DOM poza elementem DOM komponentu rodzica. Tworzysz go przez createPortal(children, domNode) z react-dom. Dzieci zachowują swoje miejsce w drzewie Reacta, więc propsy, stan i kontekst działają jak zwykle.
Kiedy używać portalu?
Gdy coś musi pojawić się nad swoim kontenerem albo poza nim: modale, tooltipy, menu rozwijane, powiadomienia (toasty). Rodzic z overflow: hidden, transform albo własnym kontekstem stosu inaczej by to przyciął albo ukrył.
Czy zdarzenia propagują się na zewnątrz portalu?
Tak, przez drzewo Reacta. Kliknięcie wewnątrz portalu dociera do handlerów onClick rodziców w React, choć węzeł DOM żyje w document.body. Natywne nasłuchiwania dodane przez addEventListener podążają za to za drzewem DOM.
Czy kontekst działa wewnątrz portalu?
Tak. Kontekst, tak jak zdarzenia, podąża za drzewem Reacta. Modal wyrenderowany do document.body nadal odczytuje motyw albo użytkownika z providerów nad komponentem, który go utworzył.
Czy do modala potrzebuję portalu?
Nie zawsze. Natywny element <dialog> otwarty przez showModal() jest rysowany w górnej warstwie przeglądarki (top layer), ponad każdym z-index, z wbudowaną obsługą fokusu. Portal to zwykły wybór dla własnych modali oraz dla tooltipów i menu.