Render Props
A render prop is a function passed to a component that the component calls to decide what to render, while the component itself owns the how — the state, effects, and event wiring. The pattern lets one component encapsulate behavior and hand the result back to the caller, who stays in full control of the markup. For years it was the go-to way to share stateful logic between components. Hooks have since taken over most of those jobs, but understanding render props still matters: you will meet them in popular libraries and they remain the cleanest fit for a few problems.
The core idea
Instead of returning JSX directly, a render-prop component invokes a function you give it and renders whatever that function returns. The component supplies live data as arguments; you supply the presentation.
import { useState } from "react";
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });
return (
<div style={{ height: "100vh" }} onMouseMove={handleMove}>
{render(pos)}
</div>
);
}
export default function App() {
return (
<MouseTracker
render={({ x, y }) => (
<h1>
Cursor at {x}, {y}
</h1>
)}
/>
);
}
MouseTracker knows how to listen for mouse movement and track coordinates. It knows nothing about how those coordinates should look on screen — that decision lives entirely in the render function the caller passes.
Children as a function
The most idiomatic form of the pattern uses children itself as the function. Because children is just a prop, it can hold a function instead of elements, which reads more naturally in JSX.
import { useState } from "react";
function Toggle({ children }) {
const [on, setOn] = useState(false);
const toggle = () => setOn((prev) => !prev);
return children({ on, toggle });
}
export default function Settings() {
return (
<Toggle>
{({ on, toggle }) => (
<button onClick={toggle}>
Notifications: {on ? "On" : "Off"}
</button>
)}
</Toggle>
);
}
Tip: The prop name does not matter to React —
render,children, or anything else works. The pattern is defined by passing a function that returns elements, not by a magic keyword.
A reusable data loader
Render props shine when the same logic must drive wildly different UI. A generic data fetcher is a classic example: it manages loading, error, and success states once, and lets each caller render those states however it likes.
import { useState, useEffect } from "react";
function Fetch({ url, children }) {
const [state, setState] = useState({
loading: true,
error: null,
data: null,
});
useEffect(() => {
let active = true;
setState({ loading: true, error: null, data: null });
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => active && setState({ loading: false, error: null, data }))
.catch((error) => active && setState({ loading: false, error, data: null }));
return () => {
active = false;
};
}, [url]);
return children(state);
}
export default function UserCard() {
return (
<Fetch url="https://api.github.com/users/facebook">
{({ loading, error, data }) => {
if (loading) return <p>Loading…</p>;
if (error) return <p>Failed: {error.message}</p>;
return (
<p>
{data.name} has {data.public_repos} public repos.
</p>
);
}}
</Fetch>
);
}
Output:
Meta Open Source has 130 public repos.
One Fetch component now serves any endpoint, and every consumer renders the loading, error, and success branches in its own way.
Why hooks largely replaced render props
The render-props pattern solved a real problem — sharing stateful logic — but it did so by adding components to the tree. Stacking several render-prop providers produces deeply nested “wrapper hell” that is awkward to read and debug. Custom hooks deliver the same reuse with none of the nesting, because a hook returns plain values into the component that calls it.
import { useState, useEffect } from "react";
function useFetch(url) {
const [state, setState] = useState({ loading: true, error: null, data: null });
useEffect(() => {
let active = true;
fetch(url)
.then((res) => res.json())
.then((data) => active && setState({ loading: false, error: null, data }))
.catch((error) => active && setState({ loading: false, error, data: null }));
return () => {
active = false;
};
}, [url]);
return state;
}
export default function UserCard() {
const { loading, error, data } = useFetch("https://api.github.com/users/facebook");
if (loading) return <p>Loading…</p>;
if (error) return <p>Failed: {error.message}</p>;
return <p>{data.name}</p>;
}
The two approaches compare cleanly:
| Concern | Render props | Custom hooks |
|---|---|---|
| Adds to component tree | Yes — a wrapper per provider | No — just a function call |
| Composition of many sources | Nested, hard to read | Flat, call several hooks in a row |
| TypeScript inference | Verbose callback types | Straightforward return types |
| Renders arbitrary JSX in place | Natural | Caller writes JSX as usual |
| Works in class components | Yes | No (hooks need function components) |
When render props still earn their place
Hooks win for logic reuse, but render props are not obsolete. They remain the right tool when the shared behavior needs to wrap or inject into the rendered output rather than just return data — for example a virtualized list that renders only visible rows, a drag-and-drop area that passes per-item handlers, or a component that needs to place children inside a specific DOM structure it controls. In those cases the component must own a slot in the tree, which a hook cannot provide.
Best practices
- Prefer a custom hook when you only need to share logic and return values; reach for render props when the component must own part of the rendered tree.
- Use
childrenas the function for the most readable call sites, and pass a single object argument so consumers can destructure exactly what they need. - Memoize the render callback or its result with
useCallback/useMemoif it is expensive, since a new function on every render forces the child to re-run. - Keep the data you pass into the function minimal and stable to avoid surprising re-renders downstream.
- Avoid stacking many render-prop providers; flatten them into hooks before nesting becomes hard to follow.
- Document the shape of the argument object so callers know exactly what fields are available.