Von HOCs und Render Props zu Custom Hooks
Wie teile ich Logik zwischen Komponenten ohne Copy-Paste? Drei React-Generationen geben dieselbe Antwort unterschiedlich – und Custom Hooks lösen es endlich ohne den verschachtelten Baum aus Wrappern.
Wenn ich in einer React-Schulung nach der Mittagspause die Frage stelle "Ihr habt jetzt in zwei Komponenten fast denselben Ladezustand mit denselben drei Zeilen useEffect. Was macht ihr damit?", dann höre ich fast immer als Erstes: "Copy-Paste, aber das fühlt sich falsch an." Genau da beginnt eine Geschichte, die React seit Jahren begleitet. Es ist im Kern immer dieselbe Frage: Wie teile ich stateful Logik zwischen Komponenten, ohne sie zu kopieren?
Interessant ist, dass React auf diese eine Frage über die Jahre drei verschiedene Antworten gegeben hat. Und wer die Reihenfolge kennt, versteht auch, warum Custom Hooks heute die naheliegende Wahl sind – und warum die älteren Muster trotzdem nicht wertlos sind.
Die immer gleiche Frage
Nehmen wir ein konkretes Beispiel, das in jeder App vorkommt: 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.
Wenn ich in Schulungen HOCs zeige, kommt regelmäßig die Frage, warum die Anwendung unbedingt außerhalb des Renders passieren muss. Die Antwort ist unangenehm konkret: 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, in jeder Schulung 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 Schulungen 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.
Wenn mich heute jemand in einer Schulung fragt, welches Muster er für geteilte Logik nehmen soll, ist die Antwort in fast allen Fällen: 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
Kommentare
Kommentar schreiben