patternjavascriptreactTip
Render props pattern for sharing stateful logic
Viewed 0 times
render propschildren as functionlogic reuseinversion of controlcustom hooks alternative
Problem
Two components need the same stateful behavior (hover detection, data fetching, resize tracking) but render completely different UI. Copy-pasting the logic duplicates code; lifting state up forces an awkward shared ancestor.
Solution
Render props: pass a function as a prop that the provider calls with the shared state:
// Hover logic extracted into a component with a render prop
function Hoverable({ children }) {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{children(isHovered)} {/ calls render prop with state /}
</div>
);
}
// Usage — two different UIs, same hover logic
<Hoverable>
{(hovered) => <Button style={{ background: hovered ? 'blue' : 'gray' }}>Hover me</Button>}
</Hoverable>
<Hoverable>
{(hovered) => <img src={hovered ? imageHovered : imageNormal} alt="" />}
</Hoverable>
// Note: In modern React, custom hooks are preferred
function useHover() {
const [isHovered, setIsHovered] = useState(false);
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
const on = () => setIsHovered(true);
const off = () => setIsHovered(false);
el.addEventListener('mouseenter', on);
el.addEventListener('mouseleave', off);
return () => { el.removeEventListener('mouseenter', on); el.removeEventListener('mouseleave', off); };
}, []);
return [ref, isHovered];
}
// Hover logic extracted into a component with a render prop
function Hoverable({ children }) {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{children(isHovered)} {/ calls render prop with state /}
</div>
);
}
// Usage — two different UIs, same hover logic
<Hoverable>
{(hovered) => <Button style={{ background: hovered ? 'blue' : 'gray' }}>Hover me</Button>}
</Hoverable>
<Hoverable>
{(hovered) => <img src={hovered ? imageHovered : imageNormal} alt="" />}
</Hoverable>
// Note: In modern React, custom hooks are preferred
function useHover() {
const [isHovered, setIsHovered] = useState(false);
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
const on = () => setIsHovered(true);
const off = () => setIsHovered(false);
el.addEventListener('mouseenter', on);
el.addEventListener('mouseleave', off);
return () => { el.removeEventListener('mouseenter', on); el.removeEventListener('mouseleave', off); };
}, []);
return [ref, isHovered];
}
Why
Render props invert control: the consumer decides what to render while the provider owns the logic. Custom hooks are now the idiomatic alternative for sharing stateful logic, but render props remain useful when the shared logic needs to provide DOM event handlers or when integrating with class components.
Gotchas
- Inline render prop functions (children={() => ...}) create new function references each render, defeating React.memo on the provider
- Custom hooks are simpler and more readable for most render-prop use cases
- Deeply nested render props create 'callback hell' — flatten with custom hooks
- The children-as-function pattern is the most common form of render props
Code Snippets
Data fetcher with render prop
// render prop
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserList users={data} />;
}}
</DataFetcher>Context
When sharing stateful UI behavior between components that render different JSX
Revisions (0)
No revisions yet.