Render Props
A component can own behavior and markup together. The toggle below owns a Boolean state, changes it after a click, and renders the only interface that can use it:
function ToggleStatus() {
const [on, setOn] = useState(false);
return (
<button onClick={() => setOn(!on)}>
Power is {on ? 'on' : 'off'}
</button>
);
}
That is a good component until another part of the interface needs the same toggle behavior with different markup. A disclosure needs a heading and panel. A settings row needs a checkbox. Copying useState and its update operation into each component would copy the protocol instead of naming it once.
We used a custom Hook before to extract behavior through a function call. There is another composition boundary worth understanding:
if a component owns behavior, how can its caller decide what that behavior renders?
Passing finished JSX #
We already know that props can carry React nodes. Start with a Toggle that receives nested JSX through children:[2]
const { useState } = React;
function Toggle({ children }) {
const [on, setOn] = useState(false);
return (
<section>
{children}
<button onClick={() => setOn((current) => !current)}>
Change Toggle state (currently {on ? 'on' : 'off'})
</button>
</section>
);
}
function App() {
return (
<Toggle>
<p>Caller output: state is unknown</p>
</Toggle>
);
}
root.render(<App />);
Waiting to run
Not run yet.
Click the button. Toggle renders again and its button reports the new state, but the paragraph remains state is unknown.
The paragraph is a complete React element created while App renders. It can use values known to App, and Toggle can choose where to place it. It has no unfinished parameter through which Toggle can supply its private on value later.
Moving the paragraph into Toggle would give it access to on, but would return us to hard-coded markup. Adding styling props such as label, onClassName, and offClassName only predicts a fixed set of variations. The caller still cannot choose an entirely different tree.
The failed attempt exposes the first constraint:
a finished React node lets the caller choose output, but gives the behavior owner no place to supply render-time information
We need the caller to pass something unfinished.
Leaving a function-shaped hole #
A JavaScript function can wait for an input. Pass one through a prop, then let Toggle call it with the state and operation it owns:
const { useState } = React;
function Toggle({ render }) {
const [on, setOn] = useState(false);
function toggle() {
setOn((current) => !current);
}
return render({ on, toggle });
}
function App() {
return (
<Toggle
render={({ on, toggle }) => (
<button
aria-pressed={on}
onClick={toggle}
>
Power is {on ? 'on' : 'off'}
</button>
)}
/>
);
}
root.render(<App />);
Waiting to run
Not run yet.
Now the caller chooses the button, its accessibility state, its text, and where the operation is attached. Toggle chooses when to call the function and which values to supply.
The render prop is not a React element. It is a function that returns one:
App creates function
|
v
Toggle calls function({ on, toggle })
|
v
function returns caller-defined React node
|
v
Toggle returns that node to React
This function-valued prop is a render prop.[1] React gives the render name no special behavior. Toggle receives an ordinary prop and calls it during its own render calculation.
That fills the first version of the hole:
a render prop leaves a function-shaped slot that the behavior owner fills with current values during rendering
Reusing behavior with unrelated output #
The separation becomes clearer when two callers use the same Toggle component to produce different host trees:
const { useState } = React;
function Toggle({ initialOn = false, render }) {
const [on, setOn] = useState(initialOn);
return render({
on,
toggle() {
setOn((current) => !current);
},
});
}
function App() {
return (
<section>
<Toggle
render={({ on, toggle }) => (
<label>
<input
type="checkbox"
checked={on}
onChange={toggle}
/>
Receive release notes
</label>
)}
/>
<Toggle
initialOn={true}
render={({ on, toggle }) => (
<section>
<h2>
<button
aria-expanded={on}
onClick={toggle}
>
Shipping details
</button>
</h2>
{on && <p>Ships in two business days.</p>}
</section>
)}
/>
</section>
);
}
root.render(<App />);
Waiting to run
Not run yet.
The checkbox and disclosure share a state transition, but not their state or markup. Each rendered Toggle occupies a different position and therefore owns an independent state value.[5]
This is the same distinction we found with custom Hooks:
reusing a behavior abstraction does not make separate uses share state
One Toggle call can give several caller-defined elements access to one state value when that is the desired ownership:
<Toggle
render={({ on, toggle }) => (
<>
<p>Power is {on ? 'on' : 'off'}</p>
<button onClick={toggle}>Change power</button>
</>
)}
/>
The function can return any React node that a component could return: one element, a fragment, text, an array, a portal, or an empty node. The render prop controls the output boundary; it is not limited to filling a single DOM slot.
Using children as the render prop #
The prop does not have to be named render. Nested JSX becomes the children prop, and that value can also be a function:[1]
const { useState } = React;
function Toggle({ children }) {
const [on, setOn] = useState(false);
return children({
on,
toggle() {
setOn((current) => !current);
},
});
}
function App() {
return (
<Toggle>
{({ on, toggle }) => (
<button onClick={toggle}>
Notifications: {on ? 'enabled' : 'disabled'}
</button>
)}
</Toggle>
);
}
root.render(<App />);
Waiting to run
Not run yet.
This is often called function as children or a children render prop. These two interfaces express the same mechanism:
<Toggle render={(model) => <View model={model} />} />
<Toggle>
{(model) => <View model={model} />}
</Toggle>
The first form names the role at the call site. It also leaves ordinary children available for some other content. The second form can read naturally when the callback is the component's one main output slot.
There is an important boundary hidden by the JSX syntax. React does not call arbitrary functions passed as children. Toggle does:
return children({ on, toggle });
Without that explicit call, the function is only a function value. A component that expects ordinary React-node children and returns {children} is not interchangeable with one whose API expects a callback.
Deciding who owns what #
The render-prop component and its caller divide responsibility:
Toggle owns |
Caller owns |
|---|---|
the on state |
the returned elements |
| the state transition | labels and semantics |
| the callback's input shape | where toggle is attached |
| when the callback is called | which supplied values it uses |
Because Toggle returns the callback result directly, it adds no host DOM node. It still adds a component boundary to the React tree, and its state attaches to that boundary.
Components like this are sometimes described as headless: they provide state or behavior while delegating most or all visual structure to the caller. “Headless” is a design description, not another React primitive.
The render prop is also an interface. Returning the raw state setter would let callers store functions, force values of the wrong domain, or couple themselves to the storage choice. Supplying { on, toggle } exposes the operation the abstraction promises instead:
render({ on, toggle })
Name the callback for the output it controls when a component has several slots:
<DataTable
renderRow={(row) => <ResultRow row={row} />}
renderEmpty={() => <p>No results</p>}
/>
Names such as renderRow, renderItem, and renderEmpty make the call contract clearer than a collection of anonymous children positions.
Following another render #
Calling a render prop is part of the component's render calculation. On a state update, React calls Toggle again, and Toggle calls the render prop again with a new snapshot:
click
-> toggle schedules state update
-> React renders Toggle with on = true
-> Toggle calls render({ on: true, toggle })
-> render prop returns the next element tree
-> React reconciles that result with the previous result
-> React commits the necessary DOM changes
The callback does not imperatively edit the earlier element. It creates the next description. React preserves or resets state inside that returned tree by the same type, position, and key rules used everywhere else.[5]
For example, switching the returned element type changes the identity at that position:
render={({ on }) => (
on ? <EnabledEditor /> : <DisabledPreview />
)}
If those component types own state, changing from one type to the other resets that state. A render prop does not create an exception to reconciliation.
Keeping the render calculation pure #
The callback runs while React is rendering Toggle. It therefore belongs to the same pure calculation as the component around it:[3]
<Toggle
render={({ on }) => {
analytics.record(on); // wrong: side effect during render
return <p>{String(on)}</p>;
}}
/>
React may run render calculations more than once, pause them, or discard their results. Put user-caused work in event handlers and synchronization in Effects, following the same rules as an ordinary component.
The callback is also not a component merely because it returns JSX. Do not call Hooks inside it:[4]
<Toggle
render={({ on }) => {
const [draft, setDraft] = useState(''); // wrong
return <Editor enabled={on} />;
}}
/>
Hooks belong at the top level of a component or custom Hook. If the caller-defined view needs Hooks, return a component whose own function calls them:
function ToggleEditor({ on }) {
const [draft, setDraft] = useState('');
return <Editor enabled={on} draft={draft} onDraftChange={setDraft} />;
}
<Toggle render={({ on }) => <ToggleEditor on={on} />} />
Now ToggleEditor is a real component boundary with a stable place for its Hook state.
Render prop or custom Hook? #
Both patterns can expose stateful behavior while leaving markup to the caller. The ownership boundary differs.
With a custom Hook, the caller is the component that owns the Hook call:
function PowerButton() {
const { on, toggle } = useToggle();
return <button onClick={toggle}>{String(on)}</button>;
}
With a render prop, another component owns the behavior and calls into the caller's output function:
function PowerButton() {
return (
<Toggle
render={({ on, toggle }) => (
<button onClick={toggle}>{String(on)}</button>
)}
/>
);
}
In modern function-component code, a custom Hook is usually the smaller abstraction when callers only need values and operations. It composes through ordinary calls and avoids an extra component and callback in the JSX tree.
A render prop remains useful when the provider needs to control when, whether, or how many times an output slot is evaluated, or when the API naturally owns a render position. Lists, data boundaries, interaction controllers, and reusable infrastructure can expose callbacks such as renderItem, renderEmpty, or renderResult without assuming the caller's markup.[1]
The choice is about where behavior and rendering ownership should meet:
| Need | Prefer |
|---|---|
| caller invokes reusable React behavior directly | custom Hook |
| component supplies data to a caller-owned output slot | render prop |
| reuse ordinary calculation with no React behavior | ordinary function |
| reuse a fixed rendered structure | component |
Avoiding accidental complexity #
A render prop should expose a coherent model rather than every detail inside a component. A callback with a long positional argument list is difficult to evolve:
render(on, toggle, enable, disable, changedAt, source)
An object makes names explicit and lets the contract grow without depending on argument position:
render({ on, toggle })
Do not introduce a render prop merely to make one line configurable. Ordinary props or composition with ordinary children are clearer when the caller only supplies a label, icon, or fixed nested content.
Also avoid deeply nesting several render-prop components when a custom Hook can flatten the same ownership:
<Mouse render={(mouse) => (
<WindowSize render={(size) => (
<Theme render={(theme) => (
<View mouse={mouse} size={size} theme={theme} />
)} />
)} />
)} />
The indentation exposes three nested ownership boundaries before the view can render. Separate custom Hooks inside View may express that relationship more directly. Render props remain a tool, not a default requirement for behavior reuse.
Filling the hole #
The finished-JSX attempt let the caller choose markup, but the element was already described before Toggle could supply its private state. A function delayed that part of the description until Toggle had the values the caller needed.
The final contract has three pieces:
function Toggle({ render }) {
const [on, setOn] = useState(false);
const model = {
on,
toggle() {
setOn((current) => !current);
},
};
return render(model);
}
Toggleowns the reusable state and transition.renderreceives the current public model during rendering.- the caller returns the React node that should represent that model.
The component supplies behavior without wrapping a component type or prescribing a host structure. It does still own a component boundary, and the returned node remains subject to ordinary rendering, purity, and reconciliation rules.
Final definition #
A render prop is a function-valued prop that a component calls during rendering to produce part or all of its React output.
The component controls the values supplied to the function. The caller controls the returned tree. A function passed as children is the same pattern when the receiving component explicitly calls it.
Summary #
Render props fill the caller-defined-output hole:
- finished JSX can choose output but cannot receive values known only inside the behavior owner
- a function can delay that output until the owner supplies its current model
renderis an ordinary prop name, not a special React APIchildrencan serve as the render prop when the component explicitly calls it- the render-prop component owns behavior while the caller owns returned markup
- separate component instances keep separate state
- one callback can return several elements that use one owner's state
- the callback runs again as part of rendering and returns a new description
- its result follows normal reconciliation and state-preservation rules
- the callback must stay pure and must not call Hooks
- a component returned by the callback can use Hooks normally
- custom Hooks are usually simpler when callers only need reusable values and operations
- render props remain useful when a component naturally controls a caller-defined output slot
Notes
- The live demos in this article use the React 19.2.5 and react-dom 19.2.5 development modules. A render prop is a composition pattern rather than a separate React API.
References
- React `Children` API: Calling a render prop to customize rendering (opens in a new tab) · Back
- React: Passing Props to a Component (opens in a new tab) · Back
- React: Components and Hooks must be pure (opens in a new tab) · Back
- React: Rules of Hooks (opens in a new tab) · Back
- React: Preserving and Resetting State (opens in a new tab) · Back