Render Props sind das zweite klassische Wiederverwendungs-Pattern aus der Vor-Hook-Zeit von React — populär gemacht durch Libraries wie React Router v4, Formik, Downshift und React Motion. Die Idee ist clever: eine Komponente kapselt Daten oder Verhalten (z.B. die aktuelle Maus-Position, den Lade-Zustand eines API-Calls, den Auf-/Zu-Status eines Dropdown), gibt diesen Zustand aber nicht als JSX zurück. Stattdessen ruft sie eine Funktion auf, die der Konsument als Prop übergibt — und die Funktion entscheidet, wie der Zustand gerendert wird. Das ergibt extrem flexible Komponenten, deren Logik wiederverwendbar ist, ohne dass die Darstellung mitgeliefert werden muss. Mit Hooks ist das Pattern heute selten geworden — aber es gibt Stellen, an denen es immer noch sinnvoll ist, vor allem dort, wo eine Library ihre Hook-Variante noch nicht hat oder wo Render-Props natürlicher lesen.
Das Pattern in einem Satz
Eine Render-Prop-Komponente nimmt eine Funktion als Prop entgegen (render, children oder ein anderer Name) und ruft sie im eigenen return auf — mit den Daten oder Helfern, die sie zur Verfügung stellt. Das Ergebnis dieses Funktionsaufrufs wird gerendert.
// Render-Prop-Komponente
function DataProvider({ render }) {
const data = useSomeData();
return render(data);
}
// Konsum
<DataProvider
render={(data) => <div>{data.title}</div>}
/>Der Konsument bekommt data als Argument der Render-Funktion und darf damit machen, was er will. Die DataProvider-Komponente weiß nichts über das JSX, das der Konsument liefert — sie ist rein für die Daten zuständig.
Beispiel — Maus-Tracker
Das klassische Render-Props-Beispiel: eine Komponente, die die aktuelle Maus-Position trackt und an Konsumenten weiterreicht.
import { useState, useEffect } from 'react';
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return render(pos);
}Zwei verschiedene Visualisierungen — dieselbe Komponente:
export default function App() {
return (
<>
{/* Variante 1: Position als Text */}
<MouseTracker
render={({ x, y }) => (
<p>Maus: {x} / {y}</p>
)}
/>
{/* Variante 2: Position visualisiert als Kreis */}
<MouseTracker
render={({ x, y }) => (
<div
style={{
position: 'fixed',
left: x,
top: y,
width: 20,
height: 20,
borderRadius: '50%',
background: 'red',
pointerEvents: 'none',
}}
/>
)}
/>
</>
);
}Was hier wichtig ist: die MouseTracker-Komponente weiß nichts von Kreisen oder Text. Sie liefert nur { x, y }. Der Konsument entscheidet komplett über die Darstellung. Genau das ist der Vorteil gegenüber einer Komponente wie <MouseDot>, die das JSX fest verdrahtet hätte.
Variante — children als Render-Funktion
Statt einer expliziten render-Prop nutzen viele Libraries children, weil das im JSX natürlicher liest:
function MouseTracker({ children }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
// children ist hier eine Funktion, kein React-Element
return children(pos);
}
// Konsum — liest sich wie eine ganz normale Verschachtelung
<MouseTracker>
{({ x, y }) => <p>Maus: {x} / {y}</p>}
</MouseTracker>Funktional identisch, aber die JSX-Lesart unterscheidet sich. React Router v4 und Downshift benutzen diese children-as-Function-Variante. Formik und React Motion haben mit render-Prop gearbeitet.
Beispiel — <Fetch> mit Render Props
Ein zweites realistisches Beispiel: eine Komponente, die Daten lädt und die drei Zustände (loading, error, success) als Argumente weiterreicht. Der Konsument rendert jeden Zustand wie er möchte.
import { useState, useEffect } from 'react';
function Fetch({ url, children }) {
const [state, setState] = useState({
data: null,
error: null,
isLoading: true,
});
useEffect(() => {
let cancelled = false;
setState({ data: null, error: null, isLoading: true });
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
if (!cancelled) setState({ data, error: null, isLoading: false });
})
.catch(error => {
if (!cancelled) setState({ data: null, error, isLoading: false });
});
return () => { cancelled = true; };
}, [url]);
return children(state);
}Konsum:
function UserProfile({ userId }) {
return (
<Fetch url={`/api/users/${userId}`}>
{({ data, error, isLoading }) => {
if (isLoading) return <Spinner />;
if (error) return <p>Fehler: {error.message}</p>;
return <h1>{data.name}</h1>;
}}
</Fetch>
);
}Das ist fast genau das, was eine moderne Library wie React Query mit Hooks anbietet — nur deklarativer im JSX und ohne den Custom-Hook-Aufruf.
Render Props vs. HOC vs. Custom Hooks
Alle drei Patterns lösen dasselbe Problem — Logik zwischen Komponenten teilen. Sie unterscheiden sich in der Mechanik:
// 1. HOC: Wrapper, der Props injiziert
const TrackedComponent = withMouse(MyComponent);
function MyComponent({ mouse }) {
return <p>{mouse.x} / {mouse.y}</p>;
}
// 2. Render Prop: Funktion bekommt Daten
function MyView() {
return (
<MouseTracker>
{(mouse) => <p>{mouse.x} / {mouse.y}</p>}
</MouseTracker>
);
}
// 3. Custom Hook: Daten als Return-Wert
function MyView() {
const mouse = useMouse();
return <p>{mouse.x} / {mouse.y}</p>;
}Vergleich:
| Aspekt | HOC | Render Props | Custom Hook |
|---|---|---|---|
| Mechanik | Funktion gibt Komponente zurück | Komponente ruft Funktion auf | Funktion gibt Werte zurück |
| Komponenten-Baum | Wrapper erzeugt Schicht | Pseudo-Schicht im JSX | Keine zusätzliche Schicht |
| Daten-Quelle für Leser | Versteckt in HOC-Komposition | Sichtbar im JSX | Sichtbar als Hook-Aufruf |
| TypeScript-Inferenz | Komplex bei Komposition | Mittel | Sauber |
| Mehrere Quellen kombinieren | Verschachtelung wird tief | „Pyramid of Doom" im JSX | Einfach: mehrere Hook-Aufrufe |
| Code-Splitting | OK | OK | OK |
| Moderner Einsatz | Nur für Wrapping-Konstrukte | Nur, wenn JSX-Komposition natürlicher liest | Default |
Die „Pyramid of Doom" bei Render Props sieht so aus, wenn man drei Quellen kombinieren will:
// Render-Prop-Variante — wird mit jeder Quelle eine Stufe tiefer
<MouseTracker>
{(mouse) => (
<Fetch url="/api/user">
{(user) => (
<ThemeConsumer>
{(theme) => (
<p style={{ color: theme.text }}>
{user.data?.name} bewegt sich bei {mouse.x}
</p>
)}
</ThemeConsumer>
)}
</Fetch>
)}
</MouseTracker>
// Hook-Variante — flach, lesbar
function App() {
const mouse = useMouse();
const user = useFetch('/api/user');
const theme = useContext(ThemeContext);
return (
<p style={{ color: theme.text }}>
{user.data?.name} bewegt sich bei {mouse.x}
</p>
);
}Genau diese „Pyramide" war der Hauptgrund, warum Hooks (React 16.8, Februar 2019) so schnell adoptiert wurden.
Wann Render Props heute noch Sinn ergeben
Trotz der allgegenwärtigen Hooks gibt es Stellen, an denen Render Props natürlicher sind:
1. JSX-zentrierte Library-APIs
Wenn eine Library möchte, dass der Konsument mehrere Sub-Komponenten mit Zustand bekommt, ist eine Render-Prop oft natürlicher als ein Hook:
// Downshift v2 (Render-Props-Stil) — die Library bestimmt JSX-Slots
<Downshift>
{({ getInputProps, getMenuProps, getItemProps, isOpen, inputValue }) => (
<div>
<input {...getInputProps()} />
<ul {...getMenuProps()}>
{isOpen && filteredItems.map((item, index) => (
<li {...getItemProps({ item, index })}>
{item.label}
</li>
))}
</ul>
</div>
)}
</Downshift>Downshift muss dem Konsumenten mehrere prop-getter-Funktionen geben — getInputProps, getMenuProps, getItemProps — und der Konsument verteilt sie auf seine JSX-Elemente. Diese „Eigentum am JSX bleibt beim Konsumenten, die Library liefert Verhalten"-Lesart passt sehr gut zu Render Props. (Auch wenn Downshift heute zusätzlich einen useCombobox-Hook anbietet.)
2. Bibliotheken, die ihre Hook-Variante (noch) nicht haben
Manche ältere Libraries (z.B. ältere Versionen von Formik) bieten primär eine Render-Prop-API. Bis man auf eine neuere Version umsteigt, arbeitet man mit dem Pattern.
3. Wenn die JSX-Komposition den Code lesbarer macht
Bei sehr deklarativem Code — etwa bei Animations-Libraries (<Transition in={open}>{state => <div>…</div>}</Transition>) — kann die Render-Prop natürlicher lesen als ein Hook plus Bedingungslogik.
Stolperfallen
PureComponent / React.memo versagt bei Inline-Render-Props
Wenn die Render-Prop direkt im JSX als Pfeilfunktion definiert wird, ist sie bei jedem Render eine neue Funktions-Referenz. Eine Memoization der Render-Prop-Komponente nützt also nichts:
const MemoizedTracker = React.memo(MouseTracker);
// Beim Re-Render des Parents wird die Pfeilfunktion neu erzeugt
// → MemoizedTracker rendert trotzdem neu, der memo() hilft nicht
<MemoizedTracker render={(pos) => <p>{pos.x}</p>} />In der Praxis ist das selten ein Performance-Problem, aber es ist ein Punkt, an dem die mentale Verwartung „memo löst Re-Renders" nicht greift.
this-Binding existiert nicht mehr, aber Closure-Capture passiert
Mit Hooks ist this-Binding kein Thema, aber jede Render-Prop ist eine Closure. Wenn der Render-Callback alte State-Werte einliest und in einem Effekt verwendet, kann es zu Stale-Closure-Bugs kommen — wie bei jedem JS-Closure auch.
Keys bei mehrfacher Verwendung
Wenn man dieselbe Render-Prop-Komponente mehrfach in einer Liste verwendet, jedes mit eigener Render-Funktion, gelten dieselben Key-Regeln wie sonst auch — Render Props sind hier keine Sonderform.
Render Prop = Funktion als Prop
Das Pattern ist nichts Magisches: die Komponente bekommt eine Funktion als Prop (entweder render, children oder ein anderer Name) und ruft sie im return mit ihren Daten als Argumenten auf. Der Konsument liefert das JSX.
`children` als Funktion liest sich oft natürlicher
Statt <X render={(data) => …} /> schreibt man <X>{(data) => …}</X>. Funktional identisch, aber im JSX sieht es aus wie eine normale Verschachtelung. React Router v4 und Downshift haben das popularisiert.
Render Props teilen Verhalten, nicht JSX
Die Render-Prop-Komponente kennt nur die Daten und die Helfer, nicht das JSX. Genau das macht das Pattern flexibel: dieselbe MouseTracker-Komponente kann einen Punkt zeichnen, einen Cursor-Text rendern oder eine Heatmap füttern.
Mehrfach-Komposition → Pyramid of Doom
Wenn man drei oder vier Render-Prop-Komponenten kombinieren will, verschachteln sich Funktionen tief. Das war der Hauptgrund, warum Hooks (mehrere Hook-Aufrufe in einer Komponente, flach) als Lösung kamen — und der Hauptgrund, warum man heute Render Props selten neu einsetzt.
`React.memo` greift nicht bei Inline-Funktionen
Eine inline-definierte Render-Prop ist bei jedem Parent-Render eine neue Referenz. React.memo auf der Render-Prop-Komponente nützt also nichts. Selten ein echtes Performance-Problem, aber gut zu wissen.
Heute: Hook by default, Render Prop nur in Spezialfällen
Custom Hook ist der moderne Standard. Render Props bleiben sinnvoll für: (a) Library-APIs mit mehreren prop-gettern (Downshift-Stil), (b) ältere Libraries ohne Hook-Variante, (c) Spezialfälle, in denen die JSX-Komposition natürlicher liest als ein Hook plus Bedingungslogik.
Render Props und Hooks schließen sich nicht aus
Eine Komponente kann intern Custom Hooks benutzen und extern eine Render-Prop-API anbieten — und umgekehrt. Viele moderne Libraries bieten beide Wege parallel an: <Downshift>{…}</Downshift> und useCombobox() für dieselbe Logik.
Verwandte Artikel
- Higher-Order Components (HOC) — das ältere Wiederverwendungs-Pattern
- Custom Hooks – Einführung — der moderne Standard
- Hook-Komposition — wie man mehrere Hooks sauber zusammenführt
- Compound Components — Komposition mit dedizierten Sub-Komponenten
Externe Quellen
- React Docs: Render Props (Legacy) — die ursprüngliche offizielle Dokumentation.
- Michael Jackson: Never Write Another HoC — der Talk, der Render Props vs. HOC popularisiert hat (2017).
- Downshift — die kanonische Render-Props-Library für Combobox/Autocomplete; bietet heute zusätzlich Hook-Varianten.
- React Router v4 Migration Guide — historischer Kontext, warum v4 stark auf Render Props gesetzt hat.