Von HOCs und Render Props zu Custom Hooks
Wie teile ich Logik zwischen Komponenten ohne Copy-Paste? Custom Hooks vermeiden dafür meist verschachtelte Wrapper; HOCs und Render Props behalten einzelne Kompositionsaufgaben.
Vier Ebenen im React-DevTools-Baum, bevor die erste erscheint, die tatsächlich etwas Sichtbares rendert: withRouter, darunter withLoading, darunter withData, und ganz unten die eigentliche Seite. Drei dieser Ebenen tragen nichts zur Darstellung bei. Sie existieren nur, um Zustand und Verhalten von außen nach innen zu reichen.
Dieser Baum ist kein Unfall, sondern die logische Folge einer Entscheidung, die React über Jahre mitgeschleppt hat. Denn stateful Logik an mehreren Stellen wiederzuverwenden, ohne sie zu duplizieren, hat das Framework in drei Generationen jeweils anders gelöst – und die ersten beiden Lösungen bezahlen dafür mit genau solchen Ebenen. Wer die Reihenfolge kennt, versteht auch, warum die älteren Muster trotzdem nicht wertlos sind.
Die immer gleiche Frage
Ein Beispiel, an dem sich das gut zeigen lässt: Eine Komponente soll wissen, ob der Browser gerade online ist. Das braucht State, das braucht einen Effekt mit Event-Listenern, und das braucht Cleanup. Wenn drei Komponenten das brauchen, will niemand denselben Block dreimal schreiben.
Die naive Lösung ist Copy-Paste. Sie funktioniert genau so lange, bis sich die Logik ändert – dann ändert man sie an einer Stelle und vergisst die anderen beiden. Ab da suchen React-Entwickler nach einem Mechanismus, der die Logik an genau einem Ort hält.
Historisch sah der Weg dorthin so aus:
graph LR A[Copy-Paste] --> B[Mixins<br/>deprecated] B --> C[HOCs &<br/>Render Props] C --> D[Custom Hooks<br/>React 16.8 / 2019]
Mixins waren die erste Antwort in den ganz frühen Tagen mit createClass, wurden aber schnell als problematisch verworfen – unklare Herkunft von Properties, Namenskollisionen, implizite Abhängigkeiten. Danach kamen zwei Muster, die lange dominierten: Higher-Order Components und Render Props. Und seit React 16.8 – stabil seit Februar 2019 – gibt es Custom Hooks. Schauen wir uns die drei relevanten Antworten der Reihe nach an.
Antwort 1: Higher-Order Components
Ein Higher-Order Component ist im Grunde ein alter Bekannter aus der funktionalen Programmierung. So wie eine Higher-Order Function eine Funktion nimmt und eine neue Funktion zurückgibt, nimmt ein HOC eine Komponente und gibt eine neue, erweiterte Komponente zurück. Die Signatur ist:
// a function that takes a component and returns a new component
const withSomething = (Component) => EnhancedComponent;
Die Namenskonvention ist withXxx. Nehmen wir das Klassikerbeispiel withLoading, das einen Ladezustand kapselt:
import { Fragment } from "react";
function withLoading(Wrapped) {
return function WithLoading(props) {
if (props.loading) {
return <Spinner />;
}
return <Wrapped {...props} />;
};
}
Angewendet wird das Ganze auf Modulebene, nicht im Render:
const UserPageWithLoading = withLoading(UserPage);
Das liest sich sauber. Und es komponiert: Man kann mehrere HOCs stapeln, withRouter(withLoading(withData(Page))). Genau da hat das Ganze aber seinen Preis.
Dass die Anwendung außerhalb des Renders passieren muss, wirkt wie eine Formalie – bis man einmal dagegen verstößt. Erzeugt man ein HOC direkt im render(), entsteht bei jedem Durchlauf eine neue Komponenten-Identität. React sieht einen anderen Typ, hängt den kompletten Subtree aus dem Baum und baut ihn neu auf – der gesamte State darunter geht verloren. Das ist einer der Bugs, die man einmal live debuggt und danach nie wieder produziert.
Dazu kommen zwei weitere Stolpersteine, die man kennen muss:
- Statische Methoden der Original-Komponente verschwinden, weil der Wrapper sie nicht automatisch mitkopiert – man braucht etwas wie
hoist-non-react-statics. - Refs gehen nicht als normale Prop durch den Wrapper hindurch. Wer eine Ref auf die innere Komponente braucht, muss
React.forwardRefbemühen (verfügbar seit React 16.3).
Und die Grundregel, die alles zusammenhält: Komposition statt Mutation. Ein HOC mutiert die übergebene Komponente nicht, es umschließt sie in einem Container. Sobald jemand anfängt, die Original-Komponente direkt zu verändern, ist das Muster kaputt.
Antwort 2: Render Props
Das zweite Muster dreht den Spieß um. Statt eine Komponente in eine andere zu wickeln, gibt man einer Komponente eine Funktion mit, die ihr sagt, was sie rendern soll. Das ist eine Render Prop: „a function prop that a component uses to know what to render“. Oft nutzt man dafür direkt children als Funktion.
class OnlineStatus extends React.Component {
state = { isOnline: false };
handleOnline = () => this.setState({ isOnline: true });
handleOffline = () => this.setState({ isOnline: false });
componentDidMount() {
window.addEventListener("online", this.handleOnline);
window.addEventListener("offline", this.handleOffline);
}
componentWillUnmount() {
window.removeEventListener("online", this.handleOnline);
window.removeEventListener("offline", this.handleOffline);
}
render() {
return this.props.children(this.state.isOnline);
}
}
Aufgerufen wird das dann so:
<OnlineStatus>
{(isOnline) => (
<span>{isOnline ? "Online" : "Offline"}</span>
)}
</OnlineStatus>
Das ist flexibler als ein HOC, weil der Aufrufer die volle Kontrolle über das Rendering behält. Aber auch hier gibt es einen Preis: Wenn man die Funktion inline im JSX erzeugt, wird sie bei jedem Render neu erstellt. Für eine PureComponent oder eine mit memo optimierte Kind-Komponente ist das Gift – der Shallow-Compare schlägt fehl, weil die Funktionsreferenz jedes Mal neu ist, und die Optimierung läuft ins Leere. Man müsste die Funktion als stabile Instanzmethode auslagern, was die vermeintliche Eleganz wieder auffrisst.
Das gemeinsame Problem: Wrapper Hell
Beide Muster – HOC und Render Props – teilen Logik über dieselbe Mechanik: über zusätzliche Ebenen im Komponentenbaum. Jedes HOC ist ein Wrapper, jede Render Prop eine weitere Verschachtelung. Bei einer, zwei Ebenen merkt man nichts. Bei fünf sieht der React-DevTools-Baum aus wie eine Zwiebel, und das nennt man liebevoll „wrapper hell“.
Genau dieser Baum ist der entscheidende Punkt. Schauen wir ihn uns an:
graph TD
subgraph HOC["HOC / Render Props: verschachtelt"]
R1[withRouter] --> L1[withLoading]
L1 --> D1[withData]
D1 --> P1[Page]
end
Jede Zwischenebene existiert nur, um Logik hereinzureichen. Sie hat keinen eigenen fachlichen Sinn, sie ist reine Infrastruktur im Baum. Und weil die Daten von außen nach innen durchgereicht werden, entstehen leicht Zwischenschichten mit veralteten Closures – ein weiterer Debugging-Klassiker.
Antwort 3: Custom Hooks
Custom Hooks lösen das Problem an einer anderen Stelle. Ein Custom Hook ist einfach eine normale JavaScript-Funktion, deren Name mit use beginnt und die andere Hooks aufrufen darf. Kein Wrapper, keine zusätzliche Komponente, keine neue Ebene im Baum. Die Logik zieht in eine Funktion um, die Komponente ruft sie auf – fertig.
Dasselbe withLoading-Problem sieht als Hook so aus, dass der Hook den Zustand liefert und die Komponente selbst entscheidet, was sie damit rendert:
import { useState, useEffect } from "react";
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return isOnline;
}
Und die Nutzung ist so unspektakulär, wie sie sein sollte:
function StatusBar() {
const isOnline = useOnlineStatus();
return <span>{isOnline ? "Online" : "Offline"}</span>;
}
Vergleicht man das mit der OnlineStatus-Render-Prop von oben: dieselbe Logik, aber ohne die Verschachtelung, ohne die inline-Funktion, ohne die Klasse. Der Baum bleibt flach:
graph TD Page2[Page] -.ruft auf.-> H1[useData] Page2 -.ruft auf.-> H2[useLoading] Page2 -.ruft auf.-> H3[useRouter]
Die Hooks hängen seitlich an der Komponente – es sind Funktionsaufrufe, keine Baumebenen. Das ist der ganze Trick, und es ist ein großer.
Zwei Eigenschaften von Custom Hooks sind es wert, explizit ausgesprochen zu werden, weil sie oft missverstanden werden:
- Custom Hooks teilen Logik, nicht State. Rufen zwei Komponenten
useOnlineStatus()auf, hat jede ihren eigenen, unabhängigen State. Der Hook ist ein Rezept, kein geteilter Topf. - Custom Hooks re-rendern mit ihrer Komponente. Bei jedem Re-Render bekommt der Hook frische Props und aktuellen State – die „veralteten Closures“, die in HOC-Zwischenschichten so leicht entstehen, sind hier deutlich seltener ein Thema.
Regeln, die auch für Custom Hooks gelten
Das use-Präfix ist keine Kosmetik. Es signalisiert dem Linter und den Lesern, dass diese Funktion selbst Hooks aufrufen darf – und dass für sie die Rules of Hooks gelten. Konkret heißt das:
Nur React-Komponenten und andere Hooks dürfen Hooks aufrufen. Eine gewöhnliche Funktion ohne use darf kein useState enthalten. Umgekehrt gilt genauso: Eine Funktion, die gar keine Hooks aufruft, sollte nicht use heißen. Denn sonst nimmt man sich die Freiheit, sie bedingt oder in einer Schleife aufzurufen – was bei echten Hooks verboten ist. Das Präfix ist also ein Vertrag in beide Richtungen. Durchgesetzt wird das Ganze vom eslint-plugin-react-hooks, das ich in keinem React-Projekt weglasse.
Eine Falle, die weniger technisch als konzeptionell ist, sehe ich in Codebasen immer wieder: der Drang, gleich generische „Lifecycle“-Hooks zu bauen, etwa ein useMount, das einen Effekt beim Mounten kapselt. Das ist meist ein schlechter Tausch. Ein Custom Hook sollte einen konkreten, benennbaren Anwendungsfall abbilden – useOnlineStatus, useChatRoom, useFormInput. Wenn der Name des Hooks bereits erklärt, was er fachlich tut, ist er gut geschnitten. Wenn er nur einen React-Mechanismus umbenennt, hat man nichts gewonnen und die Abstraktion später im Weg.
Wann HOCs heute noch sinnvoll sind
Nach all dem klingt es, als hätten Custom Hooks die beiden älteren Muster restlos ersetzt. Für das Teilen von stateful Logik stimmt das auch weitgehend. Aber es gibt eine Handvoll Aufgaben, für die HOCs weiterhin die passende Form sind – nämlich echte Cross-Cutting-Concerns, die von außen an eine Komponente herangetragen werden.
Das klassische Beispiel ist Context-Injection: ein HOC, das einer beliebigen Komponente einen Wert aus dem Context als Prop verfügbar macht, ohne dass die Komponente selbst den Context kennen muss. Solche withXxx-Wrapper begegnen einem in älteren Bibliotheken oft, und sie erfüllen dort einen Zweck, den ein Hook nicht sauber abdeckt – etwa wenn Klassenkomponenten im Spiel sind, die gar keine Hooks aufrufen können. Wer eine bestehende Codebasis mit Klassenkomponenten pflegt, wird HOCs also noch länger begegnen.
Was ich hingegen nicht mehr empfehle, ist der Griff zu HOC-Baukästen wie recompose. Die Bibliothek war zu ihrer Zeit ein cleveres Werkzeug, um Funktionen wie withState oder withReducer zu komponieren:
// recompose - historical, deprecated since 2018
import { compose, withState } from "recompose";
const enhance = compose(
withState("count", "setCount", 0)
);
Der Autor selbst hat recompose allerdings schon 2018 als deprecated markiert und dabei ausdrücklich auf Hooks als Nachfolger verwiesen. Man begegnet dem Paket also nur noch als historischem Kontext in älteren Repositories – als aktuelle Empfehlung taugt es nicht.
Der rote Faden
Drei Antworten auf eine Frage, und die Reihenfolge erzählt eine klare Geschichte. HOCs und Render Props haben gezeigt, dass sich Logik teilen lässt, ohne sie zu kopieren – aber sie haben es über den Komponentenbaum gelöst und dafür mit Verschachtelung, verlorenen Refs und statischen Methoden, aufgehobenen memo-Optimierungen und „wrapper hell“ bezahlt. Custom Hooks lösen dieselbe Aufgabe über einen simplen Funktionsaufruf und lassen den Baum flach.
Für geteilte Logik ist der erste Griff deshalb heute ein Custom Hook mit use-Präfix, der einen konkreten Use-Case kapselt. HOCs bleiben im Werkzeugkasten für die Cross-Cutting-Fälle und für die Arbeit an bestehendem Code mit Klassenkomponenten. Und Render Props tauchen noch dort auf, wo eine Komponente wirklich Kontrolle über das Rendering an den Aufrufer zurückgeben will. Aber der Standardfall – dieselbe Logik in mehreren Komponenten – hat seit 2019 eine gute, unspektakuläre Antwort. Man muss nur wissen, warum die anderen beiden vorher da waren.
Weiterführende Quellen
- Reusing Logic with Custom Hooks – das aktuelle Kapitel zu Custom Hooks
- Higher-Order Components – die historische HOC-Dokumentation
- Render Props – das Render-Props-Muster im Original
- Hooks – Custom Hooks (Legacy) – das historische Custom-Hooks-Kapitel
- Rules of Hooks / Invalid Hook Call Warning – warum das
use-Präfix und die Regeln zählen
Wie fandest du diesen Beitrag?
Kommentare