Render Props Pattern
Render props is a technique for sharing code between components using a prop whose value is a function.
Basic Render Props
jsx
1// Component with render prop
2function Mouse({ render }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 })
4
5 useEffect(() => {
6 const handleMouseMove = (e) => {
7 setPosition({ x: e.clientX, y: e.clientY })
8 }
9
10 window.addEventListener('mousemove', handleMouseMove)
11 return () => window.removeEventListener('mousemove', handleMouseMove)
12 }, [])
13
14 return render(position)
15}
16
17// Usage
18function App() {
19 return (
20 <Mouse
21 render={({ x, y }) => (
22 <div>
23 Mouse position: {x}, {y}
24 </div>
25 )}
26 />
27 )
28}Children as Function
Common variation using children:
jsx
1function Mouse({ children }) {
2 const [position, setPosition] = useState({ x: 0, y: 0 })
3
4 useEffect(() => {
5 const handleMouseMove = (e) => {
6 setPosition({ x: e.clientX, y: e.clientY })
7 }
8
9 window.addEventListener('mousemove', handleMouseMove)
10 return () => window.removeEventListener('mousemove', handleMouseMove)
11 }, [])
12
13 return children(position)
14}
15
16// Usage - cleaner syntax
17function App() {
18 return (
19 <Mouse>
20 {({ x, y }) => (
21 <div>Mouse position: {x}, {y}</div>
22 )}
23 </Mouse>
24 )
25}Toggle Component
jsx
1function Toggle({ children }) {
2 const [on, setOn] = useState(false)
3
4 const toggle = () => setOn(prev => !prev)
5 const setTrue = () => setOn(true)
6 const setFalse = () => setOn(false)
7
8 return children({ on, toggle, setTrue, setFalse })
9}
10
11// Usage
12function App() {
13 return (
14 <Toggle>
15 {({ on, toggle }) => (
16 <div>
17 <button onClick={toggle}>
18 {on ? 'ON' : 'OFF'}
19 </button>
20 {on && <div>Content is visible!</div>}
21 </div>
22 )}
23 </Toggle>
24 )
25}Fetch Component
jsx
1function Fetch({ url, children }) {
2 const [data, setData] = useState(null)
3 const [loading, setLoading] = useState(true)
4 const [error, setError] = useState(null)
5
6 useEffect(() => {
7 const fetchData = async () => {
8 try {
9 setLoading(true)
10 const response = await fetch(url)
11 const json = await response.json()
12 setData(json)
13 } catch (err) {
14 setError(err.message)
15 } finally {
16 setLoading(false)
17 }
18 }
19
20 fetchData()
21 }, [url])
22
23 return children({ data, loading, error })
24}
25
26// Usage
27function UserProfile({ userId }) {
28 return (
29 <Fetch url={`/api/users/${userId}`}>
30 {({ data, loading, error }) => {
31 if (loading) return <Spinner />
32 if (error) return <Error message={error} />
33 return <Profile user={data} />
34 }}
35 </Fetch>
36 )
37}Form Component
jsx
1function Form({ initialValues, onSubmit, children }) {
2 const [values, setValues] = useState(initialValues)
3 const [errors, setErrors] = useState({})
4
5 const handleChange = (name) => (e) => {
6 setValues(prev => ({
7 ...prev,
8 [name]: e.target.value
9 }))
10 }
11
12 const handleSubmit = (e) => {
13 e.preventDefault()
14 onSubmit(values)
15 }
16
17 return (
18 <form onSubmit={handleSubmit}>
19 {children({ values, errors, handleChange })}
20 </form>
21 )
22}
23
24// Usage
25function LoginForm() {
26 return (
27 <Form
28 initialValues={{ email: '', password: '' }}
29 onSubmit={values => console.log(values)}
30 >
31 {({ values, handleChange }) => (
32 <>
33 <input
34 value={values.email}
35 onChange={handleChange('email')}
36 placeholder="Email"
37 />
38 <input
39 type="password"
40 value={values.password}
41 onChange={handleChange('password')}
42 placeholder="Password"
43 />
44 <button type="submit">Login</button>
45 </>
46 )}
47 </Form>
48 )
49}Downshift Pattern (Advanced)
jsx
1function Autocomplete({ items, onChange, children }) {
2 const [isOpen, setIsOpen] = useState(false)
3 const [inputValue, setInputValue] = useState('')
4 const [highlightedIndex, setHighlightedIndex] = useState(0)
5
6 const filteredItems = items.filter(item =>
7 item.toLowerCase().includes(inputValue.toLowerCase())
8 )
9
10 const getInputProps = () => ({
11 value: inputValue,
12 onChange: (e) => setInputValue(e.target.value),
13 onFocus: () => setIsOpen(true),
14 onBlur: () => setTimeout(() => setIsOpen(false), 200),
15 onKeyDown: (e) => {
16 if (e.key === 'ArrowDown') {
17 setHighlightedIndex(i => Math.min(i + 1, filteredItems.length - 1))
18 } else if (e.key === 'ArrowUp') {
19 setHighlightedIndex(i => Math.max(i - 1, 0))
20 } else if (e.key === 'Enter' && isOpen) {
21 selectItem(filteredItems[highlightedIndex])
22 }
23 }
24 })
25
26 const getItemProps = (index) => ({
27 onClick: () => selectItem(filteredItems[index]),
28 className: highlightedIndex === index ? 'highlighted' : ''
29 })
30
31 const selectItem = (item) => {
32 setInputValue(item)
33 setIsOpen(false)
34 onChange(item)
35 }
36
37 return children({
38 isOpen,
39 inputValue,
40 filteredItems,
41 highlightedIndex,
42 getInputProps,
43 getItemProps
44 })
45}
46
47// Usage
48<Autocomplete items={['React', 'Vue', 'Angular']} onChange={console.log}>
49 {({ isOpen, filteredItems, getInputProps, getItemProps }) => (
50 <div>
51 <input {...getInputProps()} />
52 {isOpen && (
53 <ul>
54 {filteredItems.map((item, index) => (
55 <li key={item} {...getItemProps(index)}>{item}</li>
56 ))}
57 </ul>
58 )}
59 </div>
60 )}
61</Autocomplete>Render Props vs Hooks
Most render prop patterns can be replaced with hooks:
jsx
1// Render prop
2<Mouse>
3 {({ x, y }) => <Cursor x={x} y={y} />}
4</Mouse>
5
6// Hook equivalent
7function App() {
8 const { x, y } = useMouse()
9 return <Cursor x={x} y={y} />
10}When to use Render Props
- When you need to pass rendered output
- Component libraries with flexible rendering
- When hooks aren't possible
When to use Hooks
- Most stateful logic sharing (preferred)
- Simpler code structure
- Better composability
Render props remain useful for specific patterns!
