The render props pattern is another way to share logic between components. Instead of wrapping components (like HOCs), you pass a function as a prop that returns JSX. The component calls this function with data, letting you control what gets rendered. This pattern is flexible and powerful, though like HOCs, it's been largely replaced by hooks in modern React. Let's learn this classic pattern! šØ
What Are Render Props?
A render prop is a function prop that a component uses to know what to render:
// Component with render prop
function MouseTracker({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
// Call render function with data
return render(position)
}
// Usage: Pass function that returns JSX
function App() {
return (
<MouseTracker
render={(position) => (
<div>
Mouse is at: {position.x}, {position.y}
</div>
)}
/>
)
}
// MouseTracker handles logic, you control rendering!Render Props Definition:
A render prop is:
⢠A function passed as a prop
⢠Returns React elements (JSX)
⢠Receives data from the component
⢠Lets you control what's rendered
Pattern: Component provides data ā Function returns JSX
Why Use Render Props?
The Problem: Different UIs, Same Logic
// ā Component 1: Mouse position as coordinates
function MouseCoordinates() {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return <p>Mouse: {position.x}, {position.y}</p>
}
// ā Component 2: Mouse position as dot
function MouseDot() {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return (
<div
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: 10,
height: 10,
background: 'red'
}}
/>
)
}
// Problem: Same mouse tracking logic, duplicated!The Solution: Extract Logic with Render Props
// ā
Component that handles mouse tracking
function Mouse({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return render(position)
}
// ā
Usage 1: Render as coordinates
function App() {
return (
<Mouse
render={(pos) => (
<p>Mouse: {pos.x}, {pos.y}</p>
)}
/>
)
}
// ā
Usage 2: Render as dot
function App() {
return (
<Mouse
render={(pos) => (
<div
style={{
position: 'absolute',
left: pos.x,
top: pos.y,
width: 10,
height: 10,
background: 'red'
}}
/>
)}
/>
)
}
// Logic written once, rendered many ways!Children as Function (Function as Children)
Instead of a prop named "render", you can use the children prop:
// Component using children as function
function Mouse({ children }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
// Call children as function
return children(position)
}
// Usage: Pass function as children
function App() {
return (
<Mouse>
{(position) => (
<div>
<h1>Move your mouse!</h1>
<p>Position: {position.x}, {position.y}</p>
</div>
)}
</Mouse>
)
}
// This is called "function as children" or "children as a function"š” Render Prop vs Children
Both patterns work the same way:
Render Prop:
Children as Function:<Mouse>{(pos) => <div>...</div>}</Mouse>
Children as function is often more readable!
Common Render Props Patterns
1. Data Fetching
function DataFetcher({ url, children }) {
const [data, setData] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [url])
return children({ data, loading, error })
}
// Usage:
function App() {
return (
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}}
</DataFetcher>
)
}2. Toggle State
function Toggle({ children, initial = false }) {
const [on, setOn] = React.useState(initial)
const toggle = () => setOn(!on)
const setTrue = () => setOn(true)
const setFalse = () => setOn(false)
return children({ on, toggle, setTrue, setFalse })
}
// Usage:
function App() {
return (
<Toggle initial={false}>
{({ on, toggle, setTrue, setFalse }) => (
<div>
<p>Status: {on ? 'On' : 'Off'}</p>
<button onClick={toggle}>Toggle</button>
<button onClick={setTrue}>Turn On</button>
<button onClick={setFalse}>Turn Off</button>
</div>
)}
</Toggle>
)
}3. Window Dimensions
function WindowSize({ children }) {
const [size, setSize] = React.useState({
width: window.innerWidth,
height: window.innerHeight
})
React.useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return children(size)
}
// Usage:
function App() {
return (
<WindowSize>
{({ width, height }) => (
<div>
<p>Window size: {width} x {height}</p>
{width < 768 ? (
<p>Mobile view</p>
) : (
<p>Desktop view</p>
)}
</div>
)}
</WindowSize>
)
}4. Form State Management
function Form({ children, initialValues, onSubmit }) {
const [values, setValues] = React.useState(initialValues)
const [errors, setErrors] = React.useState({})
const handleChange = (name) => (e) => {
setValues({ ...values, [name]: e.target.value })
}
const handleSubmit = (e) => {
e.preventDefault()
onSubmit(values)
}
const validate = (name, rule) => {
if (rule && !rule(values[name])) {
setErrors({ ...errors, [name]: 'Invalid' })
} else {
const newErrors = { ...errors }
delete newErrors[name]
setErrors(newErrors)
}
}
return children({
values,
errors,
handleChange,
handleSubmit,
validate
})
}
// Usage:
function App() {
return (
<Form
initialValues={{ email: '', password: '' }}
onSubmit={(values) => console.log(values)}
>
{({ values, errors, handleChange, handleSubmit }) => (
<form onSubmit={handleSubmit}>
<input
value={values.email}
onChange={handleChange('email')}
placeholder="Email"
/>
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={values.password}
onChange={handleChange('password')}
placeholder="Password"
/>
<button type="submit">Submit</button>
</form>
)}
</Form>
)
}Comparing the Three Patterns
Same Logic, Three Approaches:
// 1. CUSTOM HOOK (Modern, Preferred)
function useMouse() {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return position
}
// Usage:
function MyComponent() {
const position = useMouse()
return <div>Mouse: {position.x}, {position.y}</div>
}
// 2. RENDER PROPS
function Mouse({ children }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return children(position)
}
// Usage:
function MyComponent() {
return (
<Mouse>
{(position) => <div>Mouse: {position.x}, {position.y}</div>}
</Mouse>
)
}
// 3. HOC
function withMouse(Component) {
return function WithMouse(props) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return <Component {...props} position={position} />
}
}
// Usage:
const MyComponentWithMouse = withMouse(MyComponent)
// COMPARISON:
// Custom Hook: Simplest, most flexible, best for new code
// Render Props: Flexible UI control, good for complex rendering logic
// HOC: Good for enhancing components, but less flexiblePros and Cons:
// CUSTOM HOOKS
ā
Simplest to use
ā
Most flexible
ā
Can return multiple values
ā
No extra nesting
ā
Best TypeScript support
ā Only for function components
// RENDER PROPS
ā
Very flexible UI control
ā
Works with class components
ā
Clear data flow
ā Creates nesting (wrapper hell)
ā Verbose syntax
ā Can impact performance
// HOC
ā
Works with class components
ā
Compose multiple HOCs
ā Props might collide
ā Wrapped component hidden in DevTools
ā Most complex pattern
ā Static methods need copyingRender Props Best Practices
- Use children as function - More readable than render prop
- Memoize render functions - Prevent unnecessary re-renders
- Keep logic in component - Render function should only render
- Name parameters clearly - Destructure for clarity
- Provide TypeScript types - Type the function signature
- Consider hooks first - Simpler for new code
- Avoid deep nesting - Multiple render props create "wrapper hell"
- Document what's provided - Clear prop descriptions
When to Use Render Props:
Consider render props when:
⢠You need very flexible UI control
⢠Multiple ways to render same data
⢠Working with class components
⢠Building a component library
Prefer custom hooks when:
⢠Writing new code
⢠Using function components
⢠Want simplicity
⢠Don't need JSX control
Common Render Props Mistakes
1. Creating Function in Render
// ā Wrong - Creates new function every render
function App() {
return (
<Mouse>
{(position) => ( // New function every time!
<div>{position.x}, {position.y}</div>
)}
</Mouse>
)
}
// This causes Mouse to re-render even if position didn't change
// ā
Better - Memoize with useCallback
function App() {
const renderMouse = React.useCallback(
(position) => <div>{position.x}, {position.y}</div>,
[]
)
return <Mouse>{renderMouse}</Mouse>
}
// ā
Or extract to separate component
function MouseDisplay({ position }) {
return <div>{position.x}, {position.y}</div>
}
function App() {
return (
<Mouse>
{(position) => <MouseDisplay position={position} />}
</Mouse>
)
}2. Wrapper Hell
// ā Wrong - Too much nesting!
function App() {
return (
<DataFetcher url="/api/user">
{({ data: user, loading: userLoading }) => (
<DataFetcher url="/api/posts">
{({ data: posts, loading: postsLoading }) => (
<WindowSize>
{({ width }) => (
<Theme>
{({ theme }) => (
// Finally render something!
<div style={{ color: theme.text }}>
{userLoading || postsLoading ? 'Loading...' :
width < 768 ? 'Mobile' : 'Desktop'
}
</div>
)}
</Theme>
)}
</WindowSize>
)}
</DataFetcher>
)}
</DataFetcher>
)
}
// ā
Better - Use custom hooks!
function App() {
const { data: user, loading: userLoading } = useFetch('/api/user')
const { data: posts, loading: postsLoading } = useFetch('/api/posts')
const { width } = useWindowSize()
const { theme } = useTheme()
return (
<div style={{ color: theme.text }}>
{userLoading || postsLoading ? 'Loading...' :
width < 768 ? 'Mobile' : 'Desktop'
}
</div>
)
}
// Much cleaner!3. Not Forwarding Props
// ā Wrong - Props lost
function Container({ children, ...props }) {
return (
<div>
{children({ data: 'some data' })}
{/* props not used! */}
</div>
)
}
// ā
Correct - Forward props
function Container({ children, className, ...props }) {
return (
<div className={className} {...props}>
{children({ data: 'some data' })}
</div>
)
}Key Takeaways
- Render props are functions that return JSX
- Pattern: Component provides data ā Function returns UI
- Children as function is cleaner than render prop
- Great for flexible UI control
- Component handles logic, you control rendering
- Can pass any data through the function
- Avoids HOC complexity
- Works with both class and function components
- Can lead to "wrapper hell" with multiple nesting
- Modern React prefers custom hooks
- Still useful for component libraries
- Memoize render functions for performance
- Clear, explicit data flow
What's Next?
Congratulations! You've completed the React Patterns section. You now understand all three classic patterns:
- Custom Hooks - Modern, preferred approach
- Higher-Order Components - Component enhancement
- Render Props - Flexible rendering control
You have a complete toolkit for code reuse in React! You know when to use each pattern and their tradeoffs. For new code, start with custom hooks - they're simpler and more flexible. But understanding HOCs and render props helps you work with existing codebases and libraries.
You've now completed 34 out of 42 tutorials - over 80% of the React tutorial series! You have strong knowledge of:
- JSX and component fundamentals
- State management and hooks
- Side effects and data fetching
- Performance optimization
- Advanced patterns and code reuse
Keep building - you're almost a React expert! š