![]()
The render props pattern is a powerful technique for sharing code between React components by using a prop that accepts a function which returns JSX. This pattern provides excellent flexibility and composability.
Core Concept
A component with a render prop takes a function that returns React elements and calls it instead of implementing its own render logic.
Basic Example
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
}
// Usage
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)}
/>
);
}
Variations of the Pattern
1. Traditional Render Prop
<DataProvider render={data => (
<ChildComponent data={data} />
)} />
2. Children as a Function
<DataProvider>
{data => (
<ChildComponent data={data} />
)}
</DataProvider>
3. Prop Name Variations
You can use any prop name, not just render:
<MousePosition>
{(position) => <DisplayPosition {...position} />}
</MousePosition>
<FetchData>
{({ loading, error, data }) => (
<Results loading={loading} error={error} data={data} />
)}
</FetchData>
Key Benefits
- Code Reusability: Share behavior across multiple components
- Flexibility: Consumers control rendering output
- Explicit Data Flow: Clear visibility of what props are being passed
- Composability: Easily combine multiple render props components
Practical Examples
1. Data Fetching Component
function FetchData({ url, children }) {
const [state, setState] = useState({
loading: true,
error: null,
data: null
});
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => setState({ loading: false, data }))
.catch(error => setState({ loading: false, error }));
}, [url]);
return children(state);
}
// Usage
<FetchData url="/api/users">
{({ loading, error, data }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserList users={data} />;
}}
</FetchData>
2. Authentication Wrapper
function Auth({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth().then(user => {
setUser(user);
setLoading(false);
});
}, []);
if (loading) return <LoadingScreen />;
return children({
user,
login: (credentials) => login(credentials).then(setUser),
logout: () => logout().then(() => setUser(null))
});
}
// Usage
<Auth>
{({ user, login, logout }) => (
user ? (
<Dashboard user={user} onLogout={logout} />
) : (
<LoginForm onLogin={login} />
)
)}
</Auth>
Best Practices
- Type Safety: Use PropTypes or TypeScript to document the render function signature
- Performance: Wrap render functions in useCallback if defined in parent component
- Composition: Combine with other patterns like hooks for maximum flexibility
- Clean API: Keep the number of arguments passed to the render function reasonable
- Error Handling: Provide graceful fallbacks for error states
TypeScript Implementation
interface MousePositionProps {
children: (position: { x: number; y: number }) => React.ReactNode;
}
function MousePosition({ children }: MousePositionProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove}>
{children(position)}
</div>
);
}
// Usage
<MousePosition>
{({ x, y }) => <div>Position: {x}, {y}</div>}
</MousePosition>
Comparison with Other Patterns
Render Props vs HOCs
| Render Props | Higher-Order Components |
|---|---|
| More flexible rendering | Fixed component structure |
| Explicit data flow | Implicit prop injection |
| Easier to compose | Can suffer from wrapper hell |
| Better TypeScript support | More complex typing |
Render Props vs Hooks
| Render Props | Custom Hooks |
|---|---|
| Works with class components | Function components only |
| Visual composition in JSX | Logic composition in code |
| More explicit data flow | More concise syntax |
Common Pitfalls
- Performance Issues: Creating new functions in render can cause unnecessary re-renders
- Solution: Memoize the render function if needed
- Over-nesting: Deeply nested render props can hurt readability
- Solution: Compose components wisely or consider hooks
- Abstraction Overuse: Not every component needs to be a render prop component
- Solution: Use only when you need true rendering flexibility
The render props pattern remains a valuable tool in React, especially when you need to provide maximum rendering flexibility to component consumers while sharing complex behavior.
