React 79 ⚛️ Render Props Pattern
The render props pattern is a technique for sharing code between React components using a prop whose value is a function. A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic . The component owns the state and behavior; the caller owns the markup. This inversion is the entire point: the component says “here is the data I have, you decide what to show,” and the caller provides a function that receives that data and returns JSX.
The pattern emerged as the answer to a specific problem in React 16: how do you share stateful logic between components without repeating it, without inheritance, and without the prop-naming collisions that plagued higher-order components? The official React documentation frames it as a solution for “cross-cutting concerns”—behavior like mouse tracking, scroll position, or data fetching that multiple components need but that does not belong to any single component’s rendering . The render prop lets that behavior live in one place while the rendering stays wherever it is needed.
Key point: A render prop is any prop whose value is a function that returns a React element. The component calls that function with its internal state and renders the result. The pattern shares behavior, not markup—the caller decides what to render, and the component decides what data to provide.
Why render props exist
The wrapper problem. Before render props, sharing stateful logic meant either copying the code into every component that needed it, or using inheritance, or using a higher-order component that wrapped the consumer and injected props. Inheritance was the wrong model for React’s composition-based architecture. HOCs worked but created layers of wrapping that became difficult to trace, and they had a subtle problem: prop name collisions. If two HOCs both injected a prop named data, the second one silently overwrote the first . Render props avoided this because the data is passed as a function argument, not as a prop name on the component itself.
The reuse problem. Consider a component that tracks the mouse position. The behavior—listening to mousemove, storing (x, y) in state, cleaning up the listener—is identical everywhere it is needed. But the rendering is different: one place shows a cat chasing the cursor, another shows a tooltip, another shows a debug readout. The official React documentation walks through exactly this example: hardcoding the cat inside the mouse component works for the cat, but then you need a separate MouseWithSomethingElse for every other use case . The render prop breaks that cycle. The Mouse component accepts a function prop, calls it with the current position, and renders whatever the function returns.
The headless UI problem. Modern component libraries—React Router, Downshift, Formik, Recharts, Visx—use render props for a specific reason: they need to provide behavior and state without dictating markup. A dropdown component that hardcodes its own menu items cannot be styled or structured differently by the consumer. A dropdown component that exposes its state through a render prop lets the consumer decide the entire DOM structure while the component handles keyboard navigation, focus management, and accessibility . This is the “headless” pattern: the logic is in the component, the markup is in the caller.
a. The basic pattern: a function prop
The component receives a function as a prop. It calls the function with whatever data it owns and renders the result.
// The component owns the mouse tracking behavior
function Mouse({ render }) {
// State that the component manages
const [position, setPosition] = useState({ x: 0, y: 0 })
// Effect that listens to the browser event
useEffect(() => {
function handleMove(event) {
// Update the position with the cursor coordinates
setPosition({ x: event.clientX, y: event.clientY })
}
// Attach the listener to the window
window.addEventListener('mousemove', handleMove)
// Clean up when the component unmounts
return () => window.removeEventListener('mousemove', handleMove)
}, [])
// Call the render prop with the current position
// The caller's function decides what JSX to return
return <>{render(position)}</>
}
// The caller decides what to render with the position
function App() {
return (
// The function receives the position and returns JSX
<Mouse
render={({ x, y }) => (
// The caller's markup can be anything
<h1>The mouse is at ({x}, {y})</h1>
)}
/>
)
}
The render prop is just a function. The Mouse component does not know or care what the function returns. It calls the function with position, and React renders whatever comes back. This is the inversion: the component that owns the state does not own the markup .
The prop does not have to be named render. Any prop whose value is a function that returns a React element qualifies as a render prop. The most common alternative is children, which produces the “children as a function” syntax: <Mouse>{({ x, y }) => <h1>...</h1>}</Mouse> . The choice between render and children is stylistic. The children form reads more naturally for components that wrap their content, while the render prop is clearer when the component has other props that matter.
b. Children as a function and the headless component pattern
The children-as-function variant is the same pattern with a different prop name. The component calls this.props.children(data) or children(data) instead of render(data).
// The Toggle component owns the boolean state and the toggle function
function Toggle({ children, initial = false }) {
// State that the component manages
const [on, setOn] = useState(initial)
// The toggle function is the behavior the caller needs
const toggle = useCallback(() => setOn((prev) => !prev), [])
// Call the children function with the state and the action
// The caller decides what the button looks like
return <>{children({ on, toggle })}</>
}
// Usage: the caller controls the entire markup
function App() {
return (
<Toggle initial={false}>
{/* The function receives on and toggle */}
{({ on, toggle }) => (
// The caller decides the button style and label
<button onClick={toggle}>
{on ? 'ON' : 'OFF'}
</button>
)}
</Toggle>
)
}
The Toggle component is a headless component. It owns the boolean state and the action that flips it, but it renders nothing of its own. The caller provides the entire UI. This is the pattern that libraries like Downshift use: the library provides the state machine for a dropdown, and the consumer provides the markup for the button, the menu, and the items .
The headless pattern is where render props remain genuinely irreplaceable. A custom hook can share the useToggle logic, but it cannot provide the component boundary that ensures the toggle state is scoped to a specific subtree, nor can it enforce that the consumer receives the state through a function that the component calls. The render prop component is the unit of composition; the hook is the unit of logic. When the composition itself is the thing being shared, the render prop is the right tool .
c. The trade-offs: nesting, function identity, and the hooks comparison
The render prop pattern has three practical costs that determine when it should be used instead of a custom hook.
The nesting problem. Each render prop component adds a layer to the component tree. Two render prop components nested produce two layers. Three produce three. The GitHub comparison of render props, HOCs, and hooks shows the pyramid that forms: <MouseTracker>{mouse => <WindowSize>{size => <Toggle>{[on, tog] => ...}</Toggle>}</WindowSize>}</MouseTracker> . The equivalent with hooks is four lines, flat, with no nesting. This is the primary reason hooks replaced render props for logic reuse. The render prop still wraps the tree; the hook does not.
The function identity problem. An inline render function (render={pos => <div>{pos.x}</div>}) creates a new function instance on every render of the parent. If the render prop component is memoized with React.memo or shouldComponentUpdate, the new function reference defeats the memoization and causes unnecessary re-renders . The fix is to define the function outside the render or memoize it with useCallback. This is a real performance cost that does not exist with hooks, because hooks do not pass functions through props.
The logic-reuse problem. The render prop pattern was designed for a time before hooks existed. For pure logic reuse—tracking a value, subscribing to an event, managing a state machine—a custom hook is simpler, composable with other hooks, and does not add a component layer . The render prop is the right choice only when the component needs to control the rendering, not just provide the data. The distinction is precise: a useMouse() hook returns { x, y } and the caller writes JSX naturally. A <Mouse> component with a render prop forces the caller to wrap their JSX in a function, which is only justified when the component also provides markup structure, context, or a boundary that the hook cannot .
The current guidance from the React community is consistent: use hooks for logic, render props for injecting markup. The render prop is not dead—it moved to a different job. Virtualized lists use renderItem, data tables use renderCell, tooltips use renderTrigger. These are cases where the component owns the behavior but the caller must control the DOM structure .
Complete Example Session
The following session builds a reusable DataFetcher component using the children-as-function pattern, then shows the equivalent custom hook to illustrate the trade-off.
// ============================================
// PART 1: THE DATA FETCHER COMPONENT
// ============================================
import { useState, useEffect, useCallback } from 'react'
// The component owns the fetching logic
// The caller owns the rendering of data, loading, and error
function DataFetcher({ url, children }) {
// The fetched data, initially null
const [data, setData] = useState(null)
// The loading flag, initially true
const [loading, setLoading] = useState(true)
// The error object, initially null
const [error, setError] = useState(null)
// The fetch function is memoized so it can be a dependency
const fetchData = useCallback(async () => {
// Reset the loading and error state before refetching
setLoading(true)
setError(null)
try {
// Send the request
const response = await fetch(url)
// Throw if the response is not OK
if (!response.ok) throw new Error('Request failed')
// Parse the JSON body
const json = await response.json()
// Store the data
setData(json)
} catch (err) {
// Store the error for the caller to render
setError(err)
} finally {
// Clear the loading flag regardless of outcome
setLoading(false)
}
}, [url])
// Fetch when the URL changes
useEffect(() => {
fetchData()
}, [fetchData])
// Call the children function with the full state
// The caller decides what to render for each state
return <>{children({ data, loading, error, refetch: fetchData })}</>
}
// ============================================
// PART 2: THE CALLER DECIDES THE MARKUP
// ============================================
function UserProfile({ userId }) {
return (
// The children function receives the state
<DataFetcher url={`/api/users/${userId}`}>
{({ data, loading, error, refetch }) => {
// The caller handles the loading state
if (loading) return <div>Loading...</div>
// The caller handles the error state
if (error) return <button onClick={refetch}>Retry</button>
// The caller handles the success state
return <h1>{data.name}</h1>
}}
</DataFetcher>
)
}
// ============================================
// PART 3: THE HOOK EQUIVALENT
// ============================================
// The same logic, extracted as a hook instead of a component
function useFetch(url) {
// Same state shape
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// Same fetch function
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) throw new Error('Request failed')
setData(await response.json())
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}, [url])
// Same effect
useEffect(() => {
fetchData()
}, [fetchData])
// Return the state as an object
return { data, loading, error, refetch: fetchData }
}
// ============================================
// PART 4: THE HOOK CALLER HAS NO WRAPPER
// ============================================
function UserProfileWithHook({ userId }) {
// Call the hook directly, no wrapping component
const { data, loading, error, refetch } = useFetch(`/api/users/${userId}`)
// The component writes its JSX naturally, no function callback
if (loading) return <div>Loading...</div>
if (error) return <button onClick={refetch}>Retry</button>
return <h1>{data.name}</h1>
}
The four parts show the same logic in two forms. The DataFetcher component owns the fetching and calls the children function with the state, leaving the caller to decide the markup. The useFetch hook owns the same logic but returns the state directly, so the caller writes JSX without a wrapper. What the session demonstrates is the precise trade-off: the render prop component adds a wrapper and forces a function callback, but it also creates a boundary that the hook cannot—a boundary that is necessary when the component itself renders markup around the caller’s output, such as a virtualized list or a tooltip.
Quick Reference
Render Prop Variants
| Prop Name | Syntax | Common Use |
|---|---|---|
render | <Comp render={data => <JSX />} /> | Explicit, when other props exist |
children | <Comp>{data => <JSX />}</Comp> | Natural for wrapper components |
| Custom name | <Comp renderItem={item => <JSX />} /> | Library APIs (lists, tables) |
When to Use Render Props
| Situation | Choice |
|---|---|
| Component owns behavior, caller controls markup | Render prop |
| Headless UI (dropdown, tooltip, list) | Render prop |
Library API with renderItem / renderCell | Render prop |
| Pure logic sharing (no markup control needed) | Custom hook |
| Simple state (toggle, counter) | Custom hook |
Trade-offs
| Aspect | Render Prop | Custom Hook |
|---|---|---|
| Component nesting | Adds a layer | No layer |
| Function identity | New function per render | No function prop |
| Logic composition | Nested render functions | Flat hook calls |
| Markup control | Caller controls fully | Caller controls fully |
| Boundary/context | Provides a component boundary | No component boundary |
Performance Notes
| Pitfall | Fix |
|---|---|
| Inline function recreates | Define outside or useCallback |
| Memoized component re-renders | Stable function reference |
| Deep nesting | Split or use hooks |
Best Practices
✅ Do This:
// Use children as function for headless components
<Toggle>{({ on, toggle }) => <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>}</Toggle>
// Memoize the render function when passing to a memoized component
const renderItem = useCallback((item) => <li>{item.name}</li>, [])
<List items={items} renderItem={renderItem} />
// Use a custom hook when only logic is shared
const { x, y } = useMouse()
❌ Don’t Do This:
// Inline render function defeats memoization
<MemoizedList renderItem={(item) => <li>{item.name}</li>} />
// Use render prop for pure logic that has no markup control
<Mouse render={({ x, y }) => <useMouseLogic x={x} y={y} />} /> // use a hook
// Nest multiple render props when a hook would flatten
<A>{a => <B>{b => <C>{c => ...}</C>}</B>}</A>
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Re-renders despite React.memo | New function reference each render | useCallback or define outside |
| Callback pyramid | Multiple nested render props | Extract to hooks or split |
| Hard to type in TypeScript | Function signature is complex | Define the render prop type explicitly |
| Component renders but caller sees nothing | Render function returns undefined | Ensure the function returns JSX |
Real-World Examples
1. Basic render prop
<Mouse render={({ x, y }) => <h1>{x}, {y}</h1>} />
2. Children as function
<Toggle>{({ on, toggle }) => <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>}</Toggle>
3. Custom prop name
<List items={data} renderItem={(item) => <Row item={item} />} />
4. Headless form validator
<FormValidator validate={validate}>
{({ values, errors, setField, submit }) => <form onSubmit={submit}>...</form>}
</FormValidator>
5. Data fetching with render prop
<DataFetcher url="/api/user">
{({ data, loading, error }) => loading ? <Spinner /> : <Profile user={data} />}
</DataFetcher>
6. Stable render function
const renderItem = useCallback((item) => <li>{item.name}</li>, [])
<MemoizedList renderItem={renderItem} />
7. Scroll tracker
<ScrollTracker>{({ scrollY }) => <Progress value={scrollY} />}</ScrollTracker>
8. The hook alternative
const { data, loading } = useFetch('/api/user')
if (loading) return <Spinner />
return <Profile user={data} />
9. Library API usage
<Recharts data={data} renderCell={({ value }) => <CustomCell value={value} />} />
10. Memoized consumer
const MemoizedRow = React.memo(({ item }) => <li>{item.name}</li>)
<List renderItem={(item) => <MemoizedRow item={item} />} />
Visual
The Inversion of Control
┌──────────────────────────────────────────────┐
│ WITHOUT RENDER PROP │
│ Component owns both state and markup │
│ → caller cannot change the markup │
│ │
│ WITH RENDER PROP │
│ Component owns state │
│ Caller owns markup │
│ → component calls the caller's function │
│ → caller returns JSX with the state │
└──────────────────────────────────────────────┘
Children as Function
┌──────────────────────────────────────────────┐
│ <Toggle> │
│ {({ on, toggle }) => ( │
│ <button onClick={toggle}> │
│ {on ? 'ON' : 'OFF'} │
│ </button> │
│ )} │
│ </Toggle> │
│ │
│ Toggle calls children({ on, toggle }) │
│ The function returns the button JSX │
└──────────────────────────────────────────────┘
Render Prop vs Hook
┌──────────────────────────────────────────────┐
│ RENDER PROP (wrapper) │
│ <Mouse>{mouse => <Cursor {...mouse} />}</Mouse>│
│ → adds a component layer │
│ → function callback │
│ │
│ HOOK (flat) │
│ const mouse = useMouse() │
│ return <Cursor {...mouse} /> │
│ → no wrapper │
│ → no function callback │
└──────────────────────────────────────────────┘
Function Identity Problem
┌──────────────────────────────────────────────┐
│ INLINE (new function every render) │
│ <MemoList renderItem={i => <Row i={i} />} />│
│ → renderItem reference changes │
│ → MemoList re-renders │
│ │
│ STABLE (useCallback) │
│ const renderItem = useCallback(i => ..., [])│
│ <MemoList renderItem={renderItem} /> │
│ → reference stable │
│ → MemoList skips re-render │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Definition | Function prop that returns JSX |
| Common names | render, children, custom |
| Purpose | Share behavior, caller controls markup |
| Headless UI | Core use case (dropdowns, lists, tooltips) |
| Hooks alternative | For pure logic reuse |
| Main cost | Component nesting |
| Function identity | Inline functions break memoization |
| Fix | useCallback or define outside render |
Key takeaways:
- A render prop is any function prop that returns a React element. The component calls the function with its state and renders the result. The prop does not have to be named
render—childrenis the most common alternative . - The pattern inverts control. The component owns the behavior and state, but the caller owns the markup. This is what makes headless UI possible: the component provides the logic, the consumer provides the DOM structure .
- Hooks replaced render props for logic reuse. The nesting pyramid of multiple render prop components is the problem hooks solve. A custom hook composes flat, with no component layer and no function callbacks .
- Render props remain the right tool for injecting markup. When the component must render its own structure around the consumer’s output—a virtualized list, a tooltip, a data table—the render prop is still the clean choice. The guidance is: hooks for logic, render props for “you tell me what to render” .
- Function identity matters for performance. An inline render function is recreated on every render, which defeats
React.memoon the receiving component. Define the function outside the render or memoize it withuseCallback. - The pattern is not dead, it moved. Library APIs like Recharts, Visx, and Downshift use render props for cell-level customization and headless components. The pattern persists where the rendering shape is the thing the caller varies .
Remember: The render prop pattern is about sharing behavior without sharing markup. The component with the render prop owns the state and the logic; the caller owns the JSX. This is the inversion that makes headless UI possible and the reason libraries like React Router and Downshift still use it. For pure logic reuse, the custom hook is simpler, flatter, and composable. The choice is not between “old” and “new”—it is between two different jobs. When the component needs to control the rendering, use a render prop. When the caller just needs the data, use a hook.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!