Higher-Order Components — kurz HOCs — sind eines der ältesten Komposition-Patterns von React. Die Idee ist denkbar einfach: eine Funktion bekommt eine Komponente, gibt eine neue Komponente zurück, die die Original-Komponente umhüllt und mit zusätzlicher Logik oder zusätzlichen Props anreichert. In der Klassen-Komponenten-Ära von React (vor 2018) waren HOCs das wichtigste Werkzeug, um wiederverwendbare Logik zwischen Komponenten zu teilen — Libraries wie connect aus Redux, withRouter aus React Router oder withStyles aus Material UI haben das Pattern populär gemacht. Mit der Einführung von Hooks in React 16.8 hat sich der Wind gedreht: heute löst man die meisten dieser Probleme mit Custom Hooks, die weniger Nestung, klarere Datenflüsse und bessere TypeScript-Inferenz bieten. Dieser Artikel zeigt das HOC-Pattern komplett — vom Aufbau über echte Use-Cases bis zu den Stellen, an denen HOCs auch im Hook-Zeitalter ihre Berechtigung behalten.
Was ein HOC ist — und was nicht
Ein Higher-Order Component ist keine Komponente, sondern eine Funktion, die nach einer mathematischen Konvention benannt ist: ein Higher-Order Function ist eine Funktion, die eine Funktion entgegennimmt oder zurückgibt. Übertragen auf React: ein HOC nimmt eine Komponente und gibt eine neue Komponente zurück.
// Signatur eines HOC
function withSomething(WrappedComponent) {
return function EnhancedComponent(props) {
// Hier: Logik, State, Effekte, Daten — vor dem Rendern
return <WrappedComponent {...props} extraProp="hi" />;
};
}
// Anwendung
const Enhanced = withSomething(MyComponent);Drei Dinge sind wichtig zu verstehen:
- Das HOC wird zur Modul-Laufzeit aufgerufen, nicht im Render.
const Enhanced = withSomething(MyComponent)läuft einmal —Enhancedist dann eine ganz normale React-Komponente, die man wie jede andere benutzt. - Die ursprüngliche Komponente wird nicht modifiziert. HOCs liefern eine neue Komponente — die alte bleibt unverändert nutzbar.
- Props werden durchgereicht. Die wichtigste Konvention ist, dass das HOC alle Props der Wrapper-Komponente an die Wrapped-Komponente weiterleitet (üblicherweise via
{...props}), und nur eigene Props hinzufügt.
Die Namens-Konvention für HOCs ist das Präfix with — withAuth, withTheme, withDataFetching. Das spiegelt die Lesart wider: „MyComponent mit Auth".
Beispiel 1 — withLogger (Trace-Pattern)
Das einfachste sinnvolle HOC: ein Logger, der jeden Render der umhüllten Komponente protokolliert. Nützlich zum Debuggen.
import { useEffect } from 'react';
export function withLogger(WrappedComponent) {
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
function WithLogger(props) {
useEffect(() => {
console.log(`[${componentName}] mounted with props:`, props);
return () => console.log(`[${componentName}] unmounted`);
}, []);
useEffect(() => {
console.log(`[${componentName}] re-rendered with props:`, props);
});
return <WrappedComponent {...props} />;
}
// Wichtig: einen sprechenden Display-Namen setzen — sonst zeigt React DevTools "Anonymous"
WithLogger.displayName = `withLogger(${componentName})`;
return WithLogger;
}Anwendung:
import { withLogger } from './withLogger';
function UserCard({ name }) {
return <div>Hallo, {name}</div>;
}
const LoggedUserCard = withLogger(UserCard);
export default function App() {
return <LoggedUserCard name="Anna" />;
}Was hier passiert:
withLogger(UserCard)läuft beim Import des Moduls und produziert eine neue KomponenteWithLogger, dieUserCardumhüllt.- Beim Mount und bei jedem Re-Render schreibt das HOC eine Zeile in die Konsole.
- Die
displayName-Zuweisung sorgt dafür, dass React DevToolswithLogger(UserCard)anzeigt — sonst stünde da nurWithLoggeroder gar nichts.
Beispiel 2 — withAuth (Authentifizierungs-Gate)
Ein realistischeres Beispiel: ein HOC, das eine Komponente nur rendert, wenn der User eingeloggt ist. Andernfalls leitet es auf die Login-Seite weiter (oder zeigt einen Hinweis).
import { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from './AuthContext';
export function withAuth(WrappedComponent, requiredRole = null) {
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
function WithAuth(props) {
const { user, isLoading } = useContext(AuthContext);
if (isLoading) {
return <div>Auth wird geprüft…</div>;
}
if (!user) {
return <Navigate to="/login" replace />;
}
if (requiredRole && user.role !== requiredRole) {
return <div>Keine Berechtigung.</div>;
}
// User berechtigt — Komponente rendern, User als Prop weiterreichen
return <WrappedComponent {...props} user={user} />;
}
WithAuth.displayName = `withAuth(${componentName})`;
return WithAuth;
}Anwendung:
import { withAuth } from './withAuth';
function ProfilePage({ user }) {
return <div>Profil von {user.name}</div>;
}
function AdminDashboard({ user }) {
return <div>Admin: {user.name}</div>;
}
// Profil: jeder eingeloggte User darf rein
const ProtectedProfile = withAuth(ProfilePage);
// Admin: nur User mit Rolle 'admin'
const ProtectedAdmin = withAuth(AdminDashboard, 'admin');
export { ProtectedProfile, ProtectedAdmin };Warum sich das gut als HOC eignet: das HOC entscheidet vor dem Rendern der Wrapped-Komponente, ob überhaupt gerendert wird. Das ist ein klassischer „Gate"-Use-Case — die Wrapped-Komponente muss von der Auth-Logik nichts wissen, der Konsument sieht beim Routing klar, welche Routen geschützt sind.
Beispiel 3 — withTheme (Context-Injection)
Vor useContext war Context-Konsum aufwendig — Context.Consumer mit Render-Prop in der JSX. HOCs haben das versteckt:
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
export function withTheme(WrappedComponent) {
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
function WithTheme(props) {
const theme = useContext(ThemeContext);
return <WrappedComponent {...props} theme={theme} />;
}
WithTheme.displayName = `withTheme(${componentName})`;
return WithTheme;
}Das HOC injiziert den theme-Wert als Prop in die Wrapped-Komponente. Genau dieses Muster hat Material UI vor v5 mit withStyles benutzt. Heute schreibt man stattdessen einfach const theme = useContext(ThemeContext) direkt in der Komponente — kein Wrapper nötig.
Stolperfallen
HOCs sind technisch unkompliziert, aber es gibt vier wiederkehrende Probleme, die bei naiver Implementierung schief gehen:
1. displayName fehlt → React DevTools zeigt „Anonymous"
Wenn das HOC einer Inner-Komponente keinen displayName setzt, zeigt React DevTools nur _default oder die anonyme Function-Expression. Das macht Debugging schwer. Lösung: immer Wrapper.displayName = "with...(" + componentName + ")" setzen.
2. Statics gehen verloren
Wenn die Wrapped-Komponente statische Methoden oder Properties trägt (z.B. MyComponent.fetchData = async () => …), gehen die beim Wrappen verloren — Enhanced hat sie nicht. Die Community-Lösung dafür ist die kleine Library hoist-non-react-statics, die alle nicht-React-Statics weiterreicht.
3. Refs werden nicht weitergereicht
Wenn der Konsument der Wrapper-Komponente ein ref übergibt, landet das ref standardmäßig auf der äußeren Komponente, nicht auf der inneren. Vor React 19 brauchte man React.forwardRef, um das zu lösen. Ab React 19 ist ref eine normale Prop — das HOC muss ref explizit durchreichen:
// React 19+: ref als normale Prop weiterreichen
export function withLogger(WrappedComponent) {
function WithLogger({ ref, ...props }) {
useEffect(() => { /* … */ }, []);
return <WrappedComponent ref={ref} {...props} />;
}
return WithLogger;
}4. Prop-Name-Kollisionen
Wenn das HOC eine Prop user injiziert und der Konsument auch <Enhanced user={…} /> schreibt, kollidieren die beiden — abhängig von der Spread-Reihenfolge gewinnt entweder die HOC-Prop oder die Konsumenten-Prop. Bei mehrfach verschachtelten HOCs (withAuth(withTheme(withLogger(Comp)))) wird das schnell unübersichtlich. Lösung: Konvention im Team, welche Props HOCs reservieren — oder besser: Hooks benutzen, dann gibt es keine Kollisionen.
HOCs vs. Custom Hooks — der moderne Stand
Für logische Wiederverwendung (Daten laden, State teilen, Subscriptions) sind Custom Hooks heute in fast allen Fällen die bessere Wahl:
// HOC-Variante (vor-Hooks-Zeit)
function withUserData(WrappedComponent) {
function WithUserData(props) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('/api/me')
.then(r => r.json())
.then(data => { setUser(data); setIsLoading(false); });
}, []);
return <WrappedComponent {...props} user={user} isLoading={isLoading} />;
}
return WithUserData;
}
// Custom-Hook-Variante (moderner Stand)
function useUserData() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('/api/me')
.then(r => r.json())
.then(data => { setUser(data); setIsLoading(false); });
}, []);
return { user, isLoading };
}
// Konsum
function Profile() {
const { user, isLoading } = useUserData();
// …
}Vorteile des Hooks:
- Keine Wrapper-Schicht im Komponentenbaum — DevTools zeigen die Komponente flach, nicht in fünf
withX-Hüllen. - TypeScript-Inferenz ist sauberer — der Rückgabewert eines Hooks ist eindeutig typisiert; HOC-Komposition erzeugt komplexe Generic-Verkettungen.
- Keine Prop-Kollisionen — Hooks geben Werte als Variablen zurück, nicht als Props auf die Komponente.
- Explizite Datenquellen — beim Lesen einer Komponente sieht man sofort, woher
userkommt (useUserData()); beim HOC muss man die Wrapper-Komposition rückwärts durchgehen.
Wann HOCs trotzdem noch sinnvoll sind:
- Rendering-Gates wie
withAuthoderwithFeatureFlag— der Punkt ist hier, dass die Komponente unter bestimmten Bedingungen gar nicht gerendert wird. Ein Hook kann das technisch auch (mit early-return im JSX), aber ein HOC drückt die Absicht deklarativer aus. - Error Boundaries und Suspense als Wrapper — z.B.
withErrorBoundary(Component)kapselt das<ErrorBoundary>-JSX. Beliebte Libraries wiereact-error-boundaryexportieren genau so ein HOC. - Komponenten-Anreicherung über externe Libraries — wenn man eine fremde Komponente nicht modifizieren kann, ist ein HOC die einzige Möglichkeit, vor/nach ihr Logik einzuschieben.
Faustregel: „Custom Hook by default, HOC nur für Wrapping-Konstrukte".
Komposition mehrerer HOCs
Wenn man mehrere HOCs verschachtelt, wird der Code schnell schwer lesbar:
// Verschachtelt — schwer lesbar
const Enhanced = withAuth(withTheme(withLogger(MyComponent)));
// Mit compose-Helper aus z.B. lodash/fp oder Redux
import { compose } from 'redux';
const enhance = compose(
withAuth,
withTheme,
withLogger,
);
const Enhanced = enhance(MyComponent);compose liest sich „von außen nach innen": withAuth(withTheme(withLogger(MyComponent))) — der withLogger ist am innersten, läuft also zuerst beim Rendern. Genau diese Lese-Reihenfolge-Verwirrung ist einer der Gründe, warum Hooks beliebter geworden sind.
`displayName` ist nicht optional
Setze für jedes HOC Wrapper.displayName = "with...(" + componentName + ")". Sonst zeigen React DevTools nur „Anonymous" oder den Funktions-Namen ohne Wrapping-Information — Debugging wird ohne Grund schwer.
HOC läuft zur Modul-Zeit, nicht im Render
const Enhanced = withX(Comp) wird einmal beim Import des Moduls ausgeführt. Niemals withX(Comp) innerhalb einer Render-Funktion aufrufen — bei jedem Render entsteht eine neue Komponente, was den State unter dir zurücksetzt und massiven Re-Render-Schaden verursacht.
Props durchreichen, nicht abschneiden
Konvention: das HOC reicht alle Props der Wrapper-Komponente an die Wrapped-Komponente weiter ({...props}) und fügt nur eigene Props hinzu. Wenn das HOC eine Prop konsumiert (z.B. isVisible), entferne sie via Destructuring (const { isVisible, ...rest } = props), damit sie nicht auch noch beim Inneren landet.
Prop-Name-Kollisionen sind ein Anzeichen
Wenn HOC-Stacks auf einmal name oder user doppelt belegen, ist das ein Zeichen dafür, dass HOCs an dieser Stelle das falsche Pattern sind. Hooks geben Werte als Variablen zurück — da gibt es kein gemeinsames Namespace.
Refs müssen explizit weitergereicht werden
Standardmäßig landet ein ref aus dem JSX auf dem äußeren Wrapper. Ab React 19 ist ref eine normale Prop: im HOC ({ ref, ...props }) destrukturieren und an die Wrapped-Komponente weiterreichen. Vor 19 brauchte es forwardRef.
Statics gehen verloren
Wenn die Wrapped-Komponente statische Methoden trägt (MyComponent.someStatic = …), sind die nach dem Wrappen weg. Die Community-Library hoist-non-react-statics löst das in einer Zeile: hoistNonReactStatics(Wrapper, WrappedComponent).
Hooks sind heute der Default
Für Logik-Wiederverwendung (Daten, State, Subscriptions): Custom Hook. HOCs reserviert man für Wrapping-Konstrukte wie Auth-Gates, Error Boundaries oder Feature-Flags — also Stellen, an denen die Wrapped-Komponente möglicherweise gar nicht gerendert wird.
Verwandte Artikel
- Custom Hooks – Einführung — der Hook-Weg zur Logik-Wiederverwendung
- Komposition statt Inheritance — das übergeordnete React-Prinzip
- Render Props — das zweite klassische Wiederverwendungs-Pattern
- Provider Pattern — Wrapping als Komposition mit Context
Externe Quellen
- React Docs: Higher-Order Components (Legacy-Doku) — die ursprüngliche Dokumentation aus der Klassen-Komponenten-Ära; immer noch die ausführlichste Referenz zum Pattern.
- Dan Abramov: Mixins Are Dead. Long Live Composition — der Artikel, der HOCs als Antwort auf Mixin-Probleme etabliert hat.
hoist-non-react-staticsauf npm — die Standard-Lösung für das Statics-Problem.- React Docs: Reusing Logic with Custom Hooks — die offizielle moderne Alternative zu HOCs.