High Order Components
High Order Components (HOCs) stellen ein fortgeschrittenes Kompositionsmuster in React dar, bei dem eine Funktion eine Komponente entgegennimmt und eine neue, erweiterte Komponente zurückgibt. Dieses Entwurfsmuster ermöglicht die elegante Kapselung wiederverwendbarer Logik, die über mehrere Komponenten hinweg geteilt werden kann. HOCs bieten eine Alternative zu Render-Props und React Hooks für das Teilen von komponentenübergreifender Funktionalität, ohne die Komponentenhierarchie unnötig zu verschachteln. Sie folgen dem Prinzip der Komposition statt Vererbung und erlauben es Entwicklern, komplexe Verhaltensweisen in kleine, wiederverwendbare Einheiten zu extrahieren.
Inhaltsverzeichnis
Einführung
Was ist ein HOC (Higher-Order Component)
Ein Higher-Order Component (HOC) ist im Grunde eine Funktion, die eine Komponente entgegennimmt und eine neue Komponente zurückgibt. Diese neue Komponente “umhüllt” die ursprüngliche Komponente und kann ihr zusätzliche Props oder Verhalten hinzufügen.
Die Kernidee ist, dass man eine bestimmte Funktionalität an mehreren Stellen wiederverwenden kann, ohne den gleichen Code immer wieder kopieren zu müssen.
Warum HOCs?
Die Motivation besteht im sogenannten DRY Prinzip (don’t repeat yourself). In jeder größeren Anwendung wird man auf Muster und Funktionalitäten stoßen, die nicht nur auf eine einzelne Komponente beschränkt sind, sondern von vielen verschiedenen Komponenten benötigt werden.
Grundstruktur
Die Struktur von HOCs und ihrer Verwendung hat eine Ähnlichkeit mit Closures. Funktionen, die andere Funktion in sich führen.
Um ein HOC zu realisieren, benötigen wir eine Funktion, die ein normales Component annimmt. Diese Funktion gibt eine andere Funktion zurück, welche wiederrum dieses normale Component anreichert (oder indirekt modifiziert) und als Rückgabewert zurückgibt. Dabei können zusätzliche Props hinzugefügt oder sonstige Modifikationen dazwischen stattfinden.
Schematischer Aufbau
// Normales Component
function Card({ props, children }) {
return <div className="card">children</div>;
}
// Wrapper Funktion
const withBackground = (Card) => {
// Neue Funktion zurückgeben.
return (props) => {
const cardStyles = {
backgroundColor: props.background || ''
};
// Normale Component mit Modifikation zurückgeben
return <Card {...props} style={cardStyles} />
};
};
// Erzeugung eine modifizierten Components
const CardWithBackground = withBackground(Card);
// Verwendung des modifizierten Components
<Card background="yellow">
Hier steht etwas
<Card>
Verwendungsfälle
Die Higher-Order Components in React können in unterschiedlichen Szenarien eingesetzt werden. Im Folgenden beschreibe ich ein paar Szenarien, in denen die Verwendung von HOCs hilfreich sein wird.
Daten laden (API)
Man hat, zum Beispiel, verschiedene Ansichten in einer App. Eine Seite zeigt eine Liste von Benutzern, eine andere zeigt eine Liste von Produkten und eine dritte zeigt die Details eines bestimmten Blog-Artikels. Alle diese Komponenten müssen Daten von einem Server abrufen.
Nehmen wir mal, die Components haben folgenden Namen:
UserList
: Liste von BenutzernProductList
: Liste von ProduktenBlogPostDetails
: Details über einen Artikel
❌ Ohne HOCs
In UserList
würde man Logik schreiben, um /api/users
abzufragen. Man bräuchte Zustände (State) für die geladenen Daten (users
), einen Ladezustand (isLoadingUsers
) und einen Fehlerzustand (usersError
). Man würde useEffect()
verwenden, um den Fetch-Aufruf zu starten.
In ProductList
müsste man fast identische Logik schreiben, nur diesmal für /api/products
, mit Zuständen wie products
, isLoadingProducts
, productsError
.
In BlogPostDetails
wieder das Gleiche für /api/posts/:id
mit Zuständen wie post
, isLoadingPost
, postError
.
✅ Mit HOCs
Man könnte ein HOC, beispielsweise withDataFetching
erstellen. Ein HOC könnte die gesamte Datenabruflogik kapseln. Man übergibt ihm die URL und die Komponente, die die Daten anzeigen soll. Das HOC kümmert sich um den Abruf, den Ladezustand und die Fehlerbehandlung und reicht die Daten (oder einen Fehler/Ladeindikator) als Prop an die ursprüngliche Komponente weiter. Die einzelnen Komponenten müssten sich dann nur noch um die Darstellung der Daten kümmern.
Nachteile ohne HOC
Würde man dieses Szenario ohne HOCs lösen, hätte man folgende mögliche Nachteile.
- Code-Duplizierung: Die Logik für das Senden der Anfrage, das Verwalten des Ladezustandes, die Fehlerbehandlung und das Aktualisieren des States ist in jeder Komponente nahezu identisch.
- Hoher Wartungsaufwand: Was, wenn sich die Art und Weise ändert, wie man Daten abruft? Vielleicht möchte man auf eine andere Bibliothek umsteigen oder einen globalen Fehler-Handler einführen. Man müsste jede einzelne Komponente anfassen und anpassen. Das könnte fehleranfällig und zeitaufwendig sein.
- Inkonsistenzen: Es ist leicht, dass sich kleine Unterschiede in der Implementierung einschleichen (z.B. wie Lade-Indikatoren angezeigt werden oder wie Fehler behandelt werden), was zu einer inkonsistenten Benutzererfahrung führt.
Authentifizierung
In diesem Verwendungsfall könnte man sich vorstellen, man hätte eine Anwendung, bei der bestimmte Bereiche nur für eingeloggte Benutzer zugänglich sein sollen. Zum Beispiel, das Benutzerprofil oder die Einstellungen. Andere Bereiche sind vielleicht nur für Administratoren gedacht.
Für diesen Fall nehmen wir mal an, wir hätten folgende Komponenten:
ProfilePage
: Seite für das BenutzerprofilSettingsPage
: Seite für die EinstellungenAdminDashboard
: Seite für die administrative Einstellungen
❌ Ohne HOC
In jeder geschützten Komponente (ProfilePage
, SettingsPage
, AdminDashboard
) müsste man in einem useEffect()
Hook (oder in einer render
Methode) prüpfen, ob der Benutzer eingeloggt ist. Diese Information könnte aus einem globalen State (Context API, Redux) oder einem LocalStorage kommen.
Wenn der Benutzer nicht eingeloggt ist, müsste man ihn auf die Login-Seite umleiten oder eine Info anzeigen, dass der Zugriff verweigert ist.
Für Admin-Bereiche käme eine zusätzliche Prüfung hinzu. Ist der Benutzer eingeloggt und ist der eingeloggte Benutzer ein Admin.
✅ Mit HOC
Ein HOC, zum Beispiel withAuth
oder requireAdmin
, würden in dieser Situation helfen. Ein HOC kann eine Komponente “umhüllen”. Das HOC für Authentifizierungs- und Autorisierungsstatus würde prüfen, ob der Benutzer berechtigt ist und bei positiver Prüfung die umhüllte Komponente rendern. Andernfalls rendert das HOC eine Umleitung zur Login-Seite oder eine Fehlermeldung. Die geschützte Komponente selbst muss sich nicht um diese Logik kümmern.
Nachteile ohne HOC
Folgende Nachteile könnten entstehen, wenn man in so einem Szenario keine HOCs einsetzt.
- Code-Duplizierung: Die Logik zur Überprüfung des Login-Status und der Benutzerrollen wiederholt sich in jeder geschützten Komponente.
- Sicherheitsrisiko: Vergisst man die Prüfung in einer einzigen Komponente, entsteht eine Sicherheitslücke.
- Schwer zu warten: Ändert sich die Art, wie der Login-Status gespeichert wird oder wie Rollen definiert sind, muss man wieder viele Komponenten anpassen.
Styling
Man möchte, zum Beispiel, dass alle Buttons in einer Anwendung ein ähnliches Grundaussehen haben oder dass Komponenten auf ein globales Theme (z.B. Hell-/Dunkelmodus, Primärfarben) reagieren.
❌ Ohne HOC
Man könnte jeder Komponente manuell Style-Props übergeben oder CSS-Klassen zuweisen.
Für Theming müsste man in jeder Komponente den aktuellen Theme-Status abfragen (z.B. aus dem Context) und die Styles entsprechend anpassen.
✅ Mit HOC
Ein HOC könnte einer Komponente automatisch themenbasierte Style-Props oder CSS-Klassen hinzufügen. Es könnte auf Änderungen im globalen Theme-Context hören und die Props entsprechend aktualisieren. Die Komponente erhält einfach die fertigen Styles als Props und muss sich nicht um die Theme-Logik kümmern.
Beispiel - Styling
In diesem Beispiel wird als Ziel gesetzt, neben einfachen Elementen-Komponents, auch modifizierte Components mit spezifischen Styles zu haben. Womöglich wird dieses Beispiel nicht wirklich eine Anwendung in realen Projekten finden, hilft aber durch die Einfachheit die Funktionsweise zu verstehen.
Alle Dateien werden in einem Ordner (hoc-example-styler
) abgelegt.
Gehen wir davon aus, dass wir zwei einfache Components haben: Link
und Paragraph
. Wie die Namen hergeben, rendert das Component Link
einfach einen Link und Paragraph
entsprechend einen Paragraphen.
Unser einfaches Link-Component sieht wie folgt aus.
function Link({
linkUrl,
linkTitle,
linkTarget = 'blank',
linkText,
style
}) {
return (
<a
href={linkUrl}
title={linkTitle}
target={linkTarget}
style={style}
>
{linkText}
</a>
);
}
export default Link;
Das einfache Paragraph-Component sieht so aus.
function Paragraph({
props,
style,
children
}) {
return (
<p style={style}>{children}</p>
);
}
export default Paragraph;
Die restliche Logik, inkl. HOC Definition werden wir in unsere Haupt-Datei führen. Wir bauen die Datei nach und nach auf, um die Entwicklung verständlicher zu machen.
Zuerst schauen wir, dass die wir die einfachen Components verwenden.
// Einfache Components importieren
import Link from './Link';
import Paragraph from './Paragraph';
function CustomStyler() {
return (
<>
<Link
linkText="Link eins"
linkTitle="Link eins"
linkUrl="https://google.com"
/>
<Paragraph>
Hier steht der erste Paragraph
</Paragraph>
</>
);
}
export default CustomStyler;
Nun möchten wir, warum auch immer, eine Version von diesen Components haben, bei der sie eine Standard-Hintergrundfarbe haben oder wir eine eigene angeben können. Dafür definieren wir die Wrapper-Funktion.
// Einfache Components importieren
import Link from './Link';
import Paragraph from './Paragraph';
const withBackground = (NormalComponent) => {
return (props) => {
const elementStyles = {
padding: '4px',
boxSizing: 'border-box',
backgroundColor: props.background || 'lightblue',
...props
};
return <WrapperComponent {...props} style={elementStyles} />
};
};
function CustomStyler() {
return (
<>
<Link
linkText="Link eins"
linkTitle="Link eins"
linkUrl="https://google.com"
/>
<Paragraph>
Hier steht der erste Paragraph
</Paragraph>
</>
);
}
export default CustomStyler;
Im nächsten Schritt müssen wir eine modifierte Version von den normalen Components erzeugen. Wir erstellen zwei neue Variablen LinkWithBackground
und ParagraphWithBackground
und weisen diesen das Ergebnis der withBackground()
Funktion zu. Der Rückgabewert dieser Funktion ist jeweils eine modifizierte oder angereicherte Version des, als Parameter, übergebenen Components.
// Einfache Components importieren
import Link from './Link';
import Paragraph from './Paragraph';
const withBackground = (NormalComponent) => {
return (props) => {
const elementStyles = {
padding: '4px',
boxSizing: 'border-box',
backgroundColor: props.background || 'lightblue',
...props
};
return <WrapperComponent {...props} style={elementStyles} />
};
};
const LinkWithBackground = withBackground(Link);
const ParagraphWithBackground = withBackground(Paragraph);
function CustomStyler() {
return (
<>
<Link
linkText="Link eins"
linkTitle="Link eins"
linkUrl="https://google.com"
/>
<Paragraph>
Hier steht der erste Paragraph
</Paragraph>
<hr />
<LinkWithBackground
linkText="Link mit Standard-Hintergrund"
linkTitle="Link mit Standard-Hintergrund"
linkUrl="https://web.de"
/>
<LinkWithBackground
linkText="Link mit anderem Hintergrund"
linkTitle="Link mit anderem Hintergrund"
linkUrl="https://apple.com"
background="lightsalmon"
/>
<ParagraphWithBackground>
Hier steht der Paragraph mit Rahmen
</ParagraphWithBackground>
</>
);
}
export default CustomStyler;
Als Ergebnis hätten wir im Browser folgendes. Die normalen Components haben lediglich die Standard-Stile (die vom Browser bestimmt werden).
Die modifizierten Components haben entsprechend angepasste Hintergründe und ein paar andere CSS-Eigenschaften.
Bedingtes Rendern
In diesem Beispiel wird eine Möglichkeit für HOC zum bedingten Rendern gezeigt. Beispielsweise möchte man eine Version eines Components erstellen, bei dem bestimmte Punkte geprüft werden, bevor dieses Component gerendert wird.
const SimpleComponent = (props) => {
return <div>Daten: {props.data}</div>
};
const withVisibility = (WrappedComponent) => {
return (props) => {
if (!props.isVisible) return null;
const { isVisible, ...restProps } = props;
return <WrappedComponent {...restProps} />
};
};
const SensitiveComponent = withVisibility(SimpleComponent);
const HocConditional = () => {
return (
<>
<SensitiveComponent isVisible={true} data="11223344" />
<SensitiveComponent isVisible={false} data="00998877" />
</>
);
};
export default HocConditional;
Als Ergebnis haben wir im Browser nur eine Ausgabe. Das Component mit isVisible={false}
wurde gar nicht gerendert.