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.