Chapter 4: Advanced Configuration with Render Props
Limitations of Elements as Props
The Problem
While elements as props work well for simple cases, they fall short when:
- State sharing is needed: Component wants to pass its internal state to the element
- Dynamic prop control: Component needs to influence element props based on its state
- API assumptions: Making assumptions about element props (like
size
andcolor
) that may not exist
Example: Button with Hover State
const Button = ({ icon }) => {
const [isHovered, setIsHovered] = useState(false);
// How do we share isHovered with the icon?
// cloneElement assumptions may break with different icon libraries
return <button onMouseOver={() => setIsHovered(true)} />;
};
The Render Props Solution
Basic Pattern
Instead of passing elements, pass functions that return elements:
// Instead of: icon={<HomeIcon />}
// Use: renderIcon={() => <HomeIcon />}
const Button = ({ renderIcon }) => {
return <button>Submit {renderIcon()}</button>;
};
// Usage
<Button renderIcon={() => <HomeIcon />} />
Passing Props to Render Functions
const Button = ({ appearance, size, renderIcon }) => {
const defaultIconProps = {
size: size === 'large' ? 'large' : 'medium',
color: appearance === 'primary' ? 'white' : 'black',
};
return (
<button>Submit {renderIcon(defaultIconProps)}</button>
);
};
// Usage - explicit prop handling
<Button renderIcon={(props) => <HomeIcon {...props} />} />
// Override specific props
<Button renderIcon={(props) => (
<HomeIcon {...props} size="large" color="red" />
)} />
// Convert props for different APIs
<Button renderIcon={(props) => (
<HomeIcon
fontSize={props.size}
style={{ color: props.color }}
/>
)} />
Sharing State
const Button = ({ renderIcon }) => {
const [isHovered, setIsHovered] = useState(false);
const iconParams = {
size: 'medium',
color: 'black',
isHovered, // Share state directly
};
return (
<button
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
Submit {renderIcon(iconParams)}
</button>
);
};
// Usage - handle state in render function
<Button renderIcon={(props) =>
props.isHovered ?
<HomeIconHovered {...props} /> :
<HomeIcon {...props} />
} />
Children as Render Props
Basic Concept
Since children
is just a prop, it can also be a function:
// These are equivalent:
<Parent children={() => <Child />} />
<Parent>{() => <Child />}</Parent>
const Parent = ({ children }) => {
return children(); // Call the function
};
Sharing Stateful Logic Example
const ResizeDetector = ({ children }) => {
const [width, setWidth] = useState();
useEffect(() => {
const listener = () => {
const width = window.innerWidth;
setWidth(width);
};
window.addEventListener("resize", listener);
return () => window.removeEventListener("resize", listener);
}, []);
return children(width); // Pass width to children
};
// Usage - no need for local state
const Layout = () => {
return (
<ResizeDetector>
{(windowWidth) =>
windowWidth > 600 ? <WideLayout /> : <NarrowLayout />
}
</ResizeDetector>
);
};
Hooks vs Render Props
The Modern Alternative
Hooks have replaced render props for most stateful logic sharing:
// Modern approach with hooks
const useResizeDetector = () => {
const [width, setWidth] = useState();
useEffect(() => {
const listener = () => {
const width = window.innerWidth;
setWidth(width);
};
window.addEventListener("resize", listener);
return () => window.removeEventListener("resize", listener);
}, []);
return width;
};
// Usage is much simpler
const Layout = () => {
const windowWidth = useResizeDetector();
return windowWidth > 600 ? <WideLayout /> : <NarrowLayout />;
};
When Render Props Are Still Useful
- Configuration and flexibility (as shown with Button example)
- Legacy codebases that heavily use the pattern
- DOM-dependent logic where you need to attach to specific elements
DOM-Dependent Example
const ScrollDetector = ({ children }) => {
const [scroll, setScroll] = useState(0);
return (
<div onScroll={(e) => setScroll(e.currentTarget.scrollTop)}>
{children(scroll)}
</div>
);
};
// Usage
const Layout = () => {
return (
<ScrollDetector>
{(scroll) => (
<>{scroll > 30 ? <SomeBlock /> : null}</>
)}
</ScrollDetector>
);
};
Key Takeaways
- Render props solve state and prop sharing that elements as props cannot handle
- Pattern structure: Pass functions instead of elements, call them with data
- Explicit data flow: Everything is visible and traceable, no hidden magic
- Children can be render props using the nested syntax
- Hooks have largely replaced render props for stateful logic sharing
- Still useful for:
- Component configuration with state sharing
- DOM-dependent logic
- Legacy codebases
- Benefits over cloneElement:
- No assumptions about element props
- Explicit prop transformation
- Clear data flow
Best Practices
- Use render props when you need to share component state with passed elements
- Prefer hooks for pure stateful logic sharing
- Use clear naming conventions (
renderIcon
,renderContent
, etc.) - Keep render functions simple and focused
- Consider render props when DOM attachment is required