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.

TypeScript HOC-Signatur.jsx
// 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:

  1. Das HOC wird zur Modul-Laufzeit aufgerufen, nicht im Render. const Enhanced = withSomething(MyComponent) läuft einmal — Enhanced ist dann eine ganz normale React-Komponente, die man wie jede andere benutzt.
  2. Die ursprüngliche Komponente wird nicht modifiziert. HOCs liefern eine neue Komponente — die alte bleibt unverändert nutzbar.
  3. 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 withwithAuth, 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.

TypeScript withLogger.jsx
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:

TypeScript App.jsx
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 Komponente WithLogger, die UserCard umhüllt.
  • Beim Mount und bei jedem Re-Render schreibt das HOC eine Zeile in die Konsole.
  • Die displayName-Zuweisung sorgt dafür, dass React DevTools withLogger(UserCard) anzeigt — sonst stünde da nur WithLogger oder 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).

TypeScript withAuth.jsx
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:

TypeScript Routes.jsx
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:

TypeScript withTheme.jsx
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:

TypeScript withLogger-ref.jsx
// 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:

TypeScript withDataFetching.jsx
// 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 user kommt (useUserData()); beim HOC muss man die Wrapper-Komposition rückwärts durchgehen.

Wann HOCs trotzdem noch sinnvoll sind:

  • Rendering-Gates wie withAuth oder withFeatureFlag — 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 wie react-error-boundary exportieren 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:

TypeScript HOC-Komposition.jsx
// 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

Externe Quellen

/ Weiter

Zurück zu Advanced Patterns

Zur Übersicht