Render props is a pattern for sharing code between components using a prop whose value is a function.
Basic Render Props#
1// Component with render prop
2class Mouse extends React.Component {
3 state = { x: 0, y: 0 };
4
5 handleMouseMove = (event) => {
6 this.setState({
7 x: event.clientX,
8 y: event.clientY
9 });
10 };
11
12 render() {
13 return (
14 <div onMouseMove={this.handleMouseMove}>
15 {this.props.render(this.state)}
16 </div>
17 );
18 }
19}
20
21// Usage
22function App() {
23 return (
24 <Mouse
25 render={({ x, y }) => (
26 <div>
27 Mouse position: {x}, {y}
28 </div>
29 )}
30 />
31 );
32}
33
34// Different rendering with same logic
35function AppWithCat() {
36 return (
37 <Mouse
38 render={({ x, y }) => (
39 <img
40 src="/cat.png"
41 style={{ position: 'absolute', left: x, top: y }}
42 />
43 )}
44 />
45 );
46}Using Children as Render Prop#
1// Using children instead of render prop
2class Mouse extends React.Component {
3 state = { x: 0, y: 0 };
4
5 handleMouseMove = (event) => {
6 this.setState({
7 x: event.clientX,
8 y: event.clientY
9 });
10 };
11
12 render() {
13 return (
14 <div onMouseMove={this.handleMouseMove}>
15 {this.props.children(this.state)}
16 </div>
17 );
18 }
19}
20
21// Cleaner usage syntax
22function App() {
23 return (
24 <Mouse>
25 {({ x, y }) => (
26 <p>Mouse: {x}, {y}</p>
27 )}
28 </Mouse>
29 );
30}Functional Component Version#
1function Mouse({ children }) {
2 const [position, setPosition] = useState({ x: 0, y: 0 });
3
4 const handleMouseMove = (event) => {
5 setPosition({
6 x: event.clientX,
7 y: event.clientY
8 });
9 };
10
11 return (
12 <div onMouseMove={handleMouseMove}>
13 {children(position)}
14 </div>
15 );
16}
17
18// Usage
19<Mouse>
20 {({ x, y }) => <Cursor x={x} y={y} />}
21</Mouse>Data Fetching Pattern#
1function DataFetcher({ url, children }) {
2 const [state, setState] = useState({
3 data: null,
4 loading: true,
5 error: null
6 });
7
8 useEffect(() => {
9 setState({ data: null, loading: true, error: null });
10
11 fetch(url)
12 .then(res => res.json())
13 .then(data => setState({ data, loading: false, error: null }))
14 .catch(error => setState({ data: null, loading: false, error }));
15 }, [url]);
16
17 return children(state);
18}
19
20// Usage
21function UserProfile({ userId }) {
22 return (
23 <DataFetcher url={`/api/users/${userId}`}>
24 {({ data, loading, error }) => {
25 if (loading) return <Spinner />;
26 if (error) return <Error message={error.message} />;
27 return <Profile user={data} />;
28 }}
29 </DataFetcher>
30 );
31}
32
33// Multiple fetchers
34function Dashboard() {
35 return (
36 <DataFetcher url="/api/stats">
37 {({ data: stats, loading: statsLoading }) => (
38 <DataFetcher url="/api/users">
39 {({ data: users, loading: usersLoading }) => {
40 if (statsLoading || usersLoading) return <Loading />;
41 return (
42 <div>
43 <Stats data={stats} />
44 <UserList users={users} />
45 </div>
46 );
47 }}
48 </DataFetcher>
49 )}
50 </DataFetcher>
51 );
52}Toggle Pattern#
1function Toggle({ children }) {
2 const [on, setOn] = useState(false);
3
4 const toggle = () => setOn(prev => !prev);
5 const setOn = () => setOn(true);
6 const setOff = () => setOn(false);
7
8 return children({
9 on,
10 toggle,
11 setOn,
12 setOff
13 });
14}
15
16// Usage
17function App() {
18 return (
19 <Toggle>
20 {({ on, toggle }) => (
21 <div>
22 <button onClick={toggle}>
23 {on ? 'Turn Off' : 'Turn On'}
24 </button>
25 {on && <div>Content is visible!</div>}
26 </div>
27 )}
28 </Toggle>
29 );
30}
31
32// Modal example
33function ModalExample() {
34 return (
35 <Toggle>
36 {({ on, toggle, setOff }) => (
37 <>
38 <button onClick={toggle}>Open Modal</button>
39 {on && (
40 <Modal onClose={setOff}>
41 <h2>Modal Content</h2>
42 <button onClick={setOff}>Close</button>
43 </Modal>
44 )}
45 </>
46 )}
47 </Toggle>
48 );
49}Form State Pattern#
1function Form({ initialValues, children }) {
2 const [values, setValues] = useState(initialValues);
3 const [errors, setErrors] = useState({});
4 const [touched, setTouched] = useState({});
5
6 const handleChange = (name) => (event) => {
7 const value = event.target.type === 'checkbox'
8 ? event.target.checked
9 : event.target.value;
10
11 setValues(prev => ({ ...prev, [name]: value }));
12 };
13
14 const handleBlur = (name) => () => {
15 setTouched(prev => ({ ...prev, [name]: true }));
16 };
17
18 const handleSubmit = (onSubmit) => (event) => {
19 event.preventDefault();
20 onSubmit(values);
21 };
22
23 const reset = () => {
24 setValues(initialValues);
25 setErrors({});
26 setTouched({});
27 };
28
29 return children({
30 values,
31 errors,
32 touched,
33 handleChange,
34 handleBlur,
35 handleSubmit,
36 reset
37 });
38}
39
40// Usage
41function LoginForm() {
42 return (
43 <Form initialValues={{ email: '', password: '' }}>
44 {({ values, handleChange, handleBlur, handleSubmit }) => (
45 <form onSubmit={handleSubmit(submitLogin)}>
46 <input
47 type="email"
48 value={values.email}
49 onChange={handleChange('email')}
50 onBlur={handleBlur('email')}
51 />
52 <input
53 type="password"
54 value={values.password}
55 onChange={handleChange('password')}
56 onBlur={handleBlur('password')}
57 />
58 <button type="submit">Login</button>
59 </form>
60 )}
61 </Form>
62 );
63}List Rendering Pattern#
1function List({ items, children, keyExtractor = item => item.id }) {
2 if (items.length === 0) {
3 return children.empty?.() || <div>No items</div>;
4 }
5
6 return (
7 <ul>
8 {items.map((item, index) => (
9 <li key={keyExtractor(item)}>
10 {children.renderItem(item, index)}
11 </li>
12 ))}
13 </ul>
14 );
15}
16
17// Usage
18function UserList({ users }) {
19 return (
20 <List
21 items={users}
22 keyExtractor={user => user.id}
23 >
24 {{
25 renderItem: (user, index) => (
26 <div>
27 <span>{index + 1}. </span>
28 <span>{user.name}</span>
29 <span> - {user.email}</span>
30 </div>
31 ),
32 empty: () => <p>No users found</p>
33 }}
34 </List>
35 );
36}Downshift-style Pattern#
1function Autocomplete({ items, onChange, children }) {
2 const [inputValue, setInputValue] = useState('');
3 const [isOpen, setIsOpen] = useState(false);
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) => {
13 setInputValue(e.target.value);
14 setIsOpen(true);
15 },
16 onFocus: () => setIsOpen(true),
17 onBlur: () => setTimeout(() => setIsOpen(false), 200)
18 });
19
20 const getItemProps = ({ item, index }) => ({
21 onClick: () => {
22 setInputValue(item);
23 onChange(item);
24 setIsOpen(false);
25 },
26 onMouseEnter: () => setHighlightedIndex(index),
27 style: {
28 backgroundColor: highlightedIndex === index ? '#eee' : 'white'
29 }
30 });
31
32 return children({
33 inputValue,
34 isOpen,
35 highlightedIndex,
36 filteredItems,
37 getInputProps,
38 getItemProps
39 });
40}
41
42// Usage
43<Autocomplete items={countries} onChange={setCountry}>
44 {({
45 inputValue,
46 isOpen,
47 filteredItems,
48 getInputProps,
49 getItemProps
50 }) => (
51 <div>
52 <input {...getInputProps()} placeholder="Select country" />
53 {isOpen && (
54 <ul>
55 {filteredItems.map((item, index) => (
56 <li key={item} {...getItemProps({ item, index })}>
57 {item}
58 </li>
59 ))}
60 </ul>
61 )}
62 </div>
63 )}
64</Autocomplete>Render Props vs Hooks#
1// Render prop version
2function WindowSize({ children }) {
3 const [size, setSize] = useState({
4 width: window.innerWidth,
5 height: window.innerHeight
6 });
7
8 useEffect(() => {
9 const handleResize = () => {
10 setSize({
11 width: window.innerWidth,
12 height: window.innerHeight
13 });
14 };
15
16 window.addEventListener('resize', handleResize);
17 return () => window.removeEventListener('resize', handleResize);
18 }, []);
19
20 return children(size);
21}
22
23// Hook version (often preferred)
24function useWindowSize() {
25 const [size, setSize] = useState({
26 width: window.innerWidth,
27 height: window.innerHeight
28 });
29
30 useEffect(() => {
31 const handleResize = () => {
32 setSize({
33 width: window.innerWidth,
34 height: window.innerHeight
35 });
36 };
37
38 window.addEventListener('resize', handleResize);
39 return () => window.removeEventListener('resize', handleResize);
40 }, []);
41
42 return size;
43}
44
45// Use the hook
46function MyComponent() {
47 const { width, height } = useWindowSize();
48 return <div>Window: {width} x {height}</div>;
49}
50
51// Wrap hook in render prop for compatibility
52function WindowSizeRenderProp({ children }) {
53 const size = useWindowSize();
54 return children(size);
55}Best Practices#
When to Use Render Props:
✓ Cross-cutting concerns
✓ Reusable stateful logic
✓ Flexible component composition
✓ When hooks aren't an option
Pattern Guidelines:
✓ Use children as function for cleaner JSX
✓ Provide sensible defaults
✓ Document the render prop API
✓ Memoize callbacks when needed
Performance:
✓ Avoid creating functions in render
✓ Use useCallback for stable references
✓ Consider React.memo for children
✓ Profile before optimizing
Prefer Hooks When:
✓ Logic can be extracted as hook
✓ No wrapper element needed
✓ Multiple consumers in same component
✓ Simpler composition
Conclusion#
Render props enable flexible component composition by passing rendering logic as a function prop. While hooks have largely replaced render props for sharing logic, the pattern remains useful for component libraries and specific use cases where flexible rendering is needed. Use children as a function for cleaner syntax, and consider converting to hooks when the logic doesn't require rendering flexibility.