Render Props
Share logic by passing a function that returns UI — still useful where a hook cannot reach.
A render prop is a component that takes a function as a prop (often children) and calls it with some internal state, letting the caller decide what to render. Before hooks, this was the main way to share stateful logic. Custom hooks have replaced most of those uses — but render props still earn their place.
Where render props still win
- You need to inject UI at a specific spot inside a component's own structure, not just consume a value. A hook returns data; a render prop lets a component hand you data and a slot to fill.
- The logic is tied to an element the component renders — measuring a DOM node, tracking pointer position over a region, virtualized rows.
The shape
<MouseTracker>
{({ x, y }) => <p>Pointer at {x}, {y}</p>}
</MouseTracker>The MouseTracker owns the listener and state; the caller owns the presentation. Data flows out through the function's arguments.
Render prop or custom hook?
If the logic produces a value the caller renders however they like, prefer a hook — it composes better and avoids the nesting. Choose a render prop when the component must provide both the logic and the surrounding markup. Many libraries expose both.
Example
import { useState, useRef, useLayoutEffect } from 'react';
// Render prop: measures its own box, then lets the caller render
// whatever they want using the measured size.
function Measure({ children }) {
const ref = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const ro = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setSize({ width: Math.round(width), height: Math.round(height) });
});
ro.observe(el);
return () => ro.disconnect();
}, []);
return <div ref={ref}>{children(size)}</div>;
}
function ResponsiveCard() {
return (
<Measure>
{({ width }) => (
<div>
{width < 400 ? <p>Compact layout</p> : <p>Wide layout</p>}
<small>{width}px available</small>
</div>
)}
</Measure>
);
}When to use it
- A <MouseTracker> component that tracks cursor position internally and calls its children function with {x, y} so any consumer can render whatever they want at that position.
- A <DataGrid> passing a renderItem render prop so product teams can inject custom cell renderers for specific columns without forking the grid component.
- A virtualized list library exposing a renderRow render prop so consumers control row markup while the library controls windowing and scroll position.
More examples
Mouse position render prop
MouseTracker owns the state and calls the children function with it, letting the consumer decide what to render at any cursor position.
function MouseTracker({ children }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
return (
<div
style={{ height: '100vh' }}
onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}
>
{children(pos)}
</div>
);
}
<MouseTracker>
{({ x, y }) => <span>Cursor: {x}, {y}</span>}
</MouseTracker>Data fetcher with render prop
Fetcher handles the async lifecycle and passes loading/data/error through a named render prop so callers own the UI for every state.
function Fetcher({ url, render }) {
const [state, setState] = React.useState({ loading: true, data: null, error: null });
React.useEffect(() => {
fetch(url)
.then((r) => r.json())
.then((data) => setState({ loading: false, data, error: null }))
.catch((error) => setState({ loading: false, data: null, error }));
}, [url]);
return render(state);
}
<Fetcher
url="/api/users"
render={({ loading, data }) => loading ? <Spinner /> : <UserList users={data} />}
/>Stable render prop with useCallback
Wrapping the render prop in useCallback prevents DataList from re-rendering every time the parent re-renders for reasons unrelated to the list.
function Parent() {
const [filter, setFilter] = React.useState('');
const renderRow = React.useCallback(
(item) => <Row key={item.id} item={item} highlight={filter} />,
[filter]
);
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<DataList items={items} renderItem={renderRow} />
</>
);
}
Discussion