Render Props Pattern
A render prop is a technique for sharing behavior between components using a prop whose value is a function that returns JSX. Instead of hardcoding what to render, a component calls a function you give it and renders whatever that function returns. The component supplies the logic; you supply the UI.
The Core Idea
Imagine you want to track the mouse position and use it in several different components. Without render props you would copy the tracking logic into each component. With render props, one component owns the logic and any number of consumers can plug in their own UI.
// The "provider" component: owns mouse-tracking logic,
// delegates rendering to the caller via a render prop
function Mouse({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
function handleMouseMove(event) {
setPosition({ x: event.clientX, y: event.clientY })
}
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{/* Call the function prop with the shared state */}
{render(position)}
</div>
)
}
// Usage: consumer decides what to render
function App() {
return (
<Mouse
render={({ x, y }) => (
<p>
Mouse is at ({x}, {y})
</p>
)}
/>
)
}The Mouse component never mentions <p> or <Cat> or any specific UI. It just tracks the mouse position and hands it to whoever is listening.
Classic Example: Mouse + Cat
The canonical render-props demo shows a cat image following the cursor. The Mouse component knows nothing about cats — it only handles events and state.
function Cat({ position }) {
return (
<img
src="/cat.png"
alt="Cat"
style={{
position: 'absolute',
left: position.x - 16,
top: position.y - 16,
width: 32,
height: 32,
}}
/>
)
}
function App() {
return (
<div style={{ position: 'relative' }}>
<Mouse render={(position) => <Cat position={position} />} />
</div>
)
}
// Later you can reuse <Mouse> with completely different UI:
function CrosshairApp() {
return (
<Mouse
render={({ x, y }) => (
<>
<div style={{ position: 'absolute', left: x, top: 0, width: 1, height: '100vh', background: 'red' }} />
<div style={{ position: 'absolute', left: 0, top: y, width: '100vw', height: 1, background: 'red' }} />
</>
)}
/>
)
}Children as a Function
A common variation uses the children prop instead of a custom prop named render. This reads more naturally in JSX because you write the function between the opening and closing tags.
// Using children as a function instead of a named render prop
function Mouse({ children }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
return (
<div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
style={{ height: '100vh' }}>
{children(position)}
</div>
)
}
// Usage — the function sits between the tags, just like a normal child
function App() {
return (
<Mouse>
{({ x, y }) => (
<p>
Cursor at ({x}, {y})
</p>
)}
</Mouse>
)
}A Reusable Toggle Component
Render props really shine when the shared behavior involves multiple pieces of state or non-trivial logic. Here is a Toggle component that manages open/closed state and exposes it through a render prop:
function Toggle({ children }) {
const [on, setOn] = React.useState(false)
return children({
on,
toggle: () => setOn((prev) => !prev),
setOn: () => setOn(true),
setOff: () => setOn(false),
})
}
// Consumer 1: a simple show/hide button
function ToggleMessage() {
return (
<Toggle>
{({ on, toggle }) => (
<div>
<button onClick={toggle}>{on ? 'Hide' : 'Show'}</button>
{on && <p>Hidden content revealed!</p>}
</div>
)}
</Toggle>
)
}
// Consumer 2: a switch that snaps open/closed
function ToggleSwitch() {
return (
<Toggle>
{({ on, toggle }) => (
<button
onClick={toggle}
style={{ background: on ? 'green' : 'gray', color: 'white' }}
>
{on ? 'ON' : 'OFF'}
</button>
)}
</Toggle>
)
}The Problem Render Props Solve
Logic reuse without inheritance — share stateful behavior across unrelated components without class inheritance or copy-pasting
Consumer controls the UI — the provider handles logic; the consumer decides how to display it, keeping concerns cleanly separated
No implicit prop merging — unlike HOCs, you can always see exactly which data the pattern injects because it is an explicit function argument
Avoids "wrapper hell" — compared to early HOC chains, a single render prop component is easier to follow in a trace
Modern Alternative: Custom Hooks
Since React 16.8, most render-prop use cases can be replaced with a custom hook. Hooks compose logic without adding wrapper nodes to the component tree, and the code is often easier to read.
// The same mouse-tracking logic as a custom hook
function useMousePosition() {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
function handleMouseMove(e) {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return position
}
// Usage: no wrapper component, no extra DOM nodes
function App() {
const { x, y } = useMousePosition()
return <p>Cursor at ({x}, {y})</p>
}
function Cat() {
const { x, y } = useMousePosition()
return (
<img
src="/cat.png"
style={{ position: 'absolute', left: x - 16, top: y - 16 }}
/>
)
}When Render Props Are Still Useful
Scenario | Why render props still fit |
|---|---|
Headless UI libraries (Downshift, React Table v7) | Library ships behavior; app supplies markup — natural API boundary |
Complex render logic with multiple slots | Pass multiple render functions for header, body, footer separately |
Wrapping third-party components that accept render props | Adapting an existing API without rewriting it |
Class components that cannot use hooks | Render props work in both class and function components |