Back to Blog
ReactRender PropsPatternsComponents

React Render Props Guide

Master the render props pattern for sharing code between React components.

B
Bootspring Team
Engineering
July 31, 2018
6 min read

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.

Share this article

Help spread the word about Bootspring