Einführung
React Portals ermöglichen das Rendern von Komponenten außerhalb der üblichen DOM-Hierarchie. Dadurch lassen sich flexible und übersichtliche Benutzeroberflächen gestalten, besonders für Modals, Tooltips oder Overlays, die unabhängig vom restlichen Layout positioniert werden müssen. Diese Technik erweitert die Möglichkeiten der Komponentenplatzierung und sorgt für mehr Struktur und Kontrolle im UI-Design.
Inhaltsverzeichnis
Was sind Portale?
React Portals sind ein Konzept, das mit React 16 eingeführt wurde und eine Möglichkeit bietet, Kind-Elemente außerhalb der DOM-Hierarchie ihrer Eltern-Komponente zu rendern. Normalerweise werden Komponenten stets innerhalb ihres Eltern-DOM-Knotens gerendert.
In manchen Situationen ist es notwendig unter bestimmten Bedingungen Components an anderen Stellen im DOM zu rendern. Hier kommen die React Portals zur Hilfe. Sie durchbrechen diese Regel, indem sie eine Einhängung eines Components bei einem beliebigen DOM-Knoten ermöglichen, unabhängig davon, wo dieser im DOM-Baum liegt.
Verwendungszweck
Warum werden Portals in React verwendet? Es gibt mehrere Gründe, warum Portals nützlich und manchmal sogar notwendig sind.
Portal API
Die Portal API in React ist überraschend einfach. Sie besteht im Wesentlichen aus einer einzigen Funktion: createPortal()
. Diese Funktion wird vom react-dom
Paket bereitgestellt.
import { createPortal } from 'react-dom';
createPortal(child, container);
Die Funktion nimmt zwei Parameter entgegen:
child
: Das zu rendernde React-Element (JSX)container
: Die DOM-Referenz, wo das Element gerendert werden soll
Der Rückgabewert von createPortal()
kann direkt in den Render-Output einer Komponente integriert werden, genau wie JSX.
Vegleich am Beispiel
Wie immer wird Theorie mit Praxis verknüpft. Wie werden hier uns zwei Beispiele anschauen, um mit eigenen Augen zu sehen, wie sich das Einhängen eines Components ohne Portale verhält und was sich ändert, wenn wir ein Portal verwenden.
Zuerst schauen wir uns an, wie das normale Verhalten bei React ist, um Components im DOM einzuhängen. Dafür werden wir eine, mehr oder weniger einfache, Components-Hierarchie aufbauen und ein Component bedingt rendern. Das bedeutet, wir wollen ein Component erst nach einem Event (Klick in diesem Fall) im DOM eingehängt haben.
import { useState } from 'react';
function ComTop({ shouldComponentInsert }) {
return (
<div className="com_top">
<ComMiddle shouldComponentInsert={shouldComponentInsert} />
</div>
);
}
function ComMiddle({ shouldComponentInsert }) {
return (
<div className="com_middle">
{shouldComponentInsert && (
<ComBottom />
)}
</div>
);
}
function ComBottom({ shouldComponentInsert }) {
return <div style={{
backgroundColor: '#e3e3e3',
padding: 20,
boxSizing: 'border-box'
}}>
Hi, ich bin ComBottom
</div>
}
function BasicExampleWithoutPortal() {
const [insertStatus, setInsertStatus] = useState(false);
function updateInsertStatus() {
setInsertStatus(currentStatus => !currentStatus);
}
return (
<div className="com_main">
<button onClick={updateInsertStatus}>Component einfügen</button>
<hr/>
<ComTop shouldComponentInsert={insertStatus} />
</div>
);
}
export default BasicExampleWithoutPortal;
Wenn wir diesen Code laufen lassen, werden wir unsere initiale Struktur sehen. Nichts besonderes soweit. Alles wie erwartet.
Nach dem Klick auf den Button “Component einfügen” wird unser DOM aktualisiert und wir sehen, dass entsprechend unserer Hierarchie eingefügt wurde. Dort, wo wir es erwarten wurden bzw. so, wie es in unserer JSX-Struktur vorgesehen war.
Wie im Abschnitt über die API von Portals geschrieben, benötigen wir ein DOM-Element, welcher unser Einhängepunkt sein wird. Wie wir ebenfalls wissen, kann dieser sich an beliebiger Stelle im DOM befinden.
Für unseren zweiten Part des Beispiel, bei welchem wir Portal erstellen und einsetzen werden, benötigen wir so ein Einhängepunkt. Und diesen erstellen wir in der index.html
des Projekts. Wir platzieren unser DOM-Element auf der Höhe des root
Elements.
Wir fügen dort dieses Element hinzu: <div id="portal-space"></div>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<div id="portal-space"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Nachdem wir das getan haben, können wir nun unser Beispiel umschreiben. Die wichtigste Änderung erfolgt an dem oder in dem Component, welches wir an einer anderen Stelle ausgeben möchten. Dabei wird das Element so ausgegeben, wie wir es an die createPortal()
Funktion übergeben.
In unserem Beispiel wird das Component ComBottom
bedingt eingehängt, nachdem ein Klick-Event dies auslöst.
So sieht unser Component ohne Verwendung von Portal aus.
function ComBottom({ shouldComponentInsert }) {
return <div style={{
backgroundColor: '#e3e3e3',
padding: 20,
boxSizing: 'border-box'
}}>
Hi, ich bin ComBottom
</div>
}
Und so wird das Component mit Verwendung von Portal aussehen.
function ComBottom({ shouldComponentInsert }) {
return (
<>
{createPortal(
<div style={{
backgroundColor: '#e3e3e3',
padding: 20,
boxSizing: 'border-box'
}}>
Hi, ich bin ComBottom
</div>,
document.getElementById('portal-space')
)}
</>
);
}
Statt also unser Component bzw. den Rückgabewert unserer Component-Funktion wie es ist zurückzugeben, übergeben wir es als ersten Parameter an die createPortal()
Funktion.
Als zweiten Parameter geben wir eine Referenz auf das Ziel-DOM-Element, in das dieses Component eingehängt werden soll. Hier haben wir direkt document.getElementById('portal-space')
verwendet. Man hätte das Erlangen der Referenz auch etwas auslagern können.
Hier ein Beispiel mit ausgelagertem Abrufen der Referenz aus das DOM-Element.
function ComBottom({ shouldComponentInsert }) {
const targetElement = document.getElementById('portal-space');
if (!targetElement) return null;
return (
<>
{createPortal(
<div style={{
backgroundColor: '#e3e3e3',
padding: 20,
boxSizing: 'border-box'
}}>
Hi, ich bin ComBottom
</div>,
targetElement
)}
</>
);
}
Dabei prüfen wir, ob wir wirklich dieses Element im DOM gefunden haben und nur dann geben wir das Component aus.
Hier nun das gesamte Beispiel mit Verwendung von Portal.
import { useState } from 'react';
import { createPortal } from 'react-dom';
function ComTop({ shouldComponentInsert }) {
return (
<div className="com_top">
<ComMiddle shouldComponentInsert={shouldComponentInsert} />
</div>
);
}
function ComMiddle({ shouldComponentInsert }) {
return (
<div className="com_middle">
{shouldComponentInsert && (
<ComBottom />
)}
</div>
);
}
function ComBottom({ shouldComponentInsert }) {
const targetElement = document.getElementById('portal-space');
if (!targetElement) return null;
return (
<>
{createPortal(
<div style={{
backgroundColor: '#e3e3e3',
padding: 20,
boxSizing: 'border-box'
}}>
Hi, ich bin ComBottom
</div>,
targetElement
)}
</>
);
}
function BasicExampleWithPortal() {
const [insertStatus, setInsertStatus] = useState(false);
function updateInsertStatus() {
setInsertStatus(currentStatus => !currentStatus);
}
return (
<div className="com_main">
<button onClick={updateInsertStatus}>Component einfügen</button>
<hr/>
<ComTop shouldComponentInsert={insertStatus} />
</div>
);
}
export default BasicExampleWithPortal;
Als Ergebnis haben wir initial im Grunde die identische Struktur, wie auch beim ersten Beispiel, ohne Verwendung von Portal. An der initialen Struktur selbst hat sich nichts geändert.
Wenn wir wieder auf den Button “Component einfügen” klicken, wird unser ComBottom
eingehägt. Allerdings nicht mehr an der Position, an der beim Standard-Verhalten einfügen würde, sondern in unserem portal-space
Element.