Poznajemy funkcję createPortal z react-dom, która pozwala nam renderować DOM gdziekolwiek wewnątrz drzewa DOM pliku HTML, z którego korzysta nasz projekt Reacta. Do dzieła.
Ok, przypomnijmy:
- Mamy komponent App.jsx, który jest naszą apką, głównym RFC
- Mamy plik main.jsx, który importuje główny komponent, wyszukuje w index.html element o ID root i renderuje tam główny komponent App, opleciony przez Strict Mode
- Mamy ten index.html, w którym znajduje się cały html, w tym element o id root (tam jest mountowana nasza apka) oraz inne elementy HTML, do których możemy teleportować treść za pomocą createPortal
Funkcja createPortal przyjmuje JSX oraz selektor, który dotyczy index.html i pokazuje, gdzie mamy ten JSX teleportować:
import { createPortal } from 'react-dom';
function MyComponent() {
return (
<div style={{ border: '2px solid black' }}>
<p>This child is placed in the parent div.</p>
{createPortal(
<p>This child is placed in the document body.</p>,
document.body
)}
</div>
);
}
Tutaj podaliśmy, że po prostu do body, także przekazaliśmy tylko markup HTML, choć z JSX createPortal też by sobie poradziło. Wynik HTML wygląda tak:
<body>
<div id="root">
...
<div style="border: 2px solid black">
<p>This child is placed inside the parent div.</p>
</div>
...
</div>
<p>This child is placed in the document body.</p>
</body>
Czyli nasz kontent ląduje poza Apką, poza rootem, do którego App jest mountowany. Ten selektor odnosi się do index.html i jego drzewa DOM.
Przykład jakiegoś modala z dokumentacji Reacta:
export default function ModalContent({ onClose }) {
return (
<div className="modal">
<div>I'm a modal dialog</div>
<button onClick={onClose}>Close</button>
</div>
);
}
I komponent, który z niego korzysta:
import { useState } from 'react';
import { createPortal } from 'react-dom';
import ModalContent from './ModalContent.js';
export default function PortalExample() {
const [showModal, setShowModal] = useState(false);
return (
<>
<button onClick={() => setShowModal(true)}>
Show modal using a portal
</button>
{showModal && createPortal(
<ModalContent onClose={() => setShowModal(false)} />,
document.body
)}
</>
);
}
Ten komponent (bez ModalContent) wyświetla się w swoim rodzicu i idąc po rodzicach prędzej czy później dojdziemy do RFC App. Zaś idąc po DOM pokazywanym w przeglądarce prędzej czy później dojdziemy do elementu o id root.
ModalContent natomiast jest pokazywany, w zależności od tego jaki mamy stan jego RFC rodzica, poza rodzicem. W ogóle poza elementem root, w którym żyje nasza apka.
I choć jest pokazywany tam, poza rodzicem RFC, poza rootem, w którym żyje App RFC, to:
- Bez problemu jest renderowany do HTML, choć sam jest komponentem JSX
- Nadal jest powiązany ze swoim rodzicem RFC, który przekazuje mu przez props onClose, którego nadal on może użyć
- Zwróćmy uwagę, w jaki sposób ten przepływ działa:
- ModalContent jest renderowany wtedy, gdy showModal jest prawdziwe
- ModalContent jest komponentem RFC
- ModalContent jest renderowany poza rodzicem oraz poza miejscem, w którym żyje Apka
- Mimo wszystko, jest w stanie zamienić swoje JSX na funkcjonalny HTML
- Nadal ma powiązania z rodzicem, od którego bierze propsy i któremu może stan zmienić
- Poprzez zmianę stanu rodzica wywołuje diff-render rodzica, który powoduje, że on sam znika, gdy doszło do zamknięcia
Ciekawe, jak twórcy Reacta to osiągnęli, w każdym razie to działa i jest na swój sposób piękne. Pamiętajmy, createPortal to nie tylko wywalenie jakiegoś HTML poza miejsce, w którym żyje nasza Apka, bo to ręcznie moglibyśmy do index.html dopisać.
Portal to teleportowanie w pełni funkcjonalnego komponentu JSX poza miejsce, w którym żyje nasza Apka, ale na poziomie hierarchii komponentów w taki sposób, jakby nadal na swoim miejscu był.
Po prostu renderowany jest gdzie indziej. Jeszcze jeden przykład z dokumentacji Reacta, index.html:
<!DOCTYPE html>
<html>
<head><title>My app</title></head>
<body>
<h1>Welcome to my hybrid app</h1>
<div class="parent">
<div class="sidebar">
This is server non-React markup
<div id="sidebar-content"></div>
</div>
<div id="root"></div>
</div>
</body>
</html>
Sztywnego HTMLa możemy sobie sami dopisać i tak zrobiliśmy. Dla dynamicznej treści pozostawiliśmy placeholder na sidebar-content:
import { createPortal } from 'react-dom';
const sidebarContentEl = document.getElementById('sidebar-content');
export default function App() {
return (
<>
<MainContent />
{createPortal(
<SidebarContent />,
sidebarContentEl
)}
</>
);
}
function MainContent() {
return <p>This part is rendered by React</p>;
}
function SidebarContent() {
return <p>This part is also rendered by React!</p>;
}
Tu za bardzo nie ma dynamiki, ale już wiemy, że:
- SidebarContent może być dynamicznym elementem JSX, nie musi być kodem HTML
- Na poziomie hierarchii DOM będzie wyświetlał się w divie sidebar
- Na poziomie hierarchii komponentów, jego rodzicem nadal będzie RFC App, od którego może brać propsy, któremu może zmieniać stan, i tak dalej.
Mam nadzieję, że nie jest to szczególnie trudne.