Back to Blog
ReactPatternsComponentsComposition

React Render Props Pattern

Master the React render props pattern. From basic usage to advanced patterns for component composition.

B
Bootspring Team
Engineering
August 9, 2020
7 min read

Render props enable flexible component composition through function children. Here's how to use them effectively.

Basic Render Props#

1// Basic render prop component 2interface MousePosition { 3 x: number; 4 y: number; 5} 6 7interface MouseTrackerProps { 8 render: (position: MousePosition) => React.ReactNode; 9} 10 11function MouseTracker({ render }: MouseTrackerProps) { 12 const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 }); 13 14 useEffect(() => { 15 const handleMouseMove = (e: MouseEvent) => { 16 setPosition({ x: e.clientX, y: e.clientY }); 17 }; 18 19 window.addEventListener('mousemove', handleMouseMove); 20 return () => window.removeEventListener('mousemove', handleMouseMove); 21 }, []); 22 23 return <>{render(position)}</>; 24} 25 26// Usage 27function App() { 28 return ( 29 <MouseTracker 30 render={({ x, y }) => ( 31 <div> 32 Mouse position: {x}, {y} 33 </div> 34 )} 35 /> 36 ); 37}

Children as Function#

1// Using children as render prop 2interface MouseTrackerProps { 3 children: (position: MousePosition) => React.ReactNode; 4} 5 6function MouseTracker({ children }: MouseTrackerProps) { 7 const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 }); 8 9 useEffect(() => { 10 const handleMouseMove = (e: MouseEvent) => { 11 setPosition({ x: e.clientX, y: e.clientY }); 12 }; 13 14 window.addEventListener('mousemove', handleMouseMove); 15 return () => window.removeEventListener('mousemove', handleMouseMove); 16 }, []); 17 18 return <>{children(position)}</>; 19} 20 21// Usage - cleaner syntax 22function App() { 23 return ( 24 <MouseTracker> 25 {({ x, y }) => ( 26 <div> 27 Mouse position: {x}, {y} 28 </div> 29 )} 30 </MouseTracker> 31 ); 32}

Data Fetching#

1interface FetchProps<T> { 2 url: string; 3 children: (state: { 4 data: T | null; 5 loading: boolean; 6 error: Error | null; 7 refetch: () => void; 8 }) => React.ReactNode; 9} 10 11function Fetch<T>({ url, children }: FetchProps<T>) { 12 const [data, setData] = useState<T | null>(null); 13 const [loading, setLoading] = useState(true); 14 const [error, setError] = useState<Error | null>(null); 15 16 const fetchData = useCallback(async () => { 17 setLoading(true); 18 setError(null); 19 20 try { 21 const response = await fetch(url); 22 if (!response.ok) throw new Error('Request failed'); 23 const json = await response.json(); 24 setData(json); 25 } catch (err) { 26 setError(err as Error); 27 } finally { 28 setLoading(false); 29 } 30 }, [url]); 31 32 useEffect(() => { 33 fetchData(); 34 }, [fetchData]); 35 36 return <>{children({ data, loading, error, refetch: fetchData })}</>; 37} 38 39// Usage 40function UserProfile({ userId }: { userId: string }) { 41 return ( 42 <Fetch<User> url={`/api/users/${userId}`}> 43 {({ data, loading, error, refetch }) => { 44 if (loading) return <Spinner />; 45 if (error) return <ErrorMessage error={error} onRetry={refetch} />; 46 if (!data) return null; 47 48 return ( 49 <div> 50 <h1>{data.name}</h1> 51 <p>{data.email}</p> 52 </div> 53 ); 54 }} 55 </Fetch> 56 ); 57}

Toggle Pattern#

1interface ToggleProps { 2 initialValue?: boolean; 3 children: (state: { 4 on: boolean; 5 toggle: () => void; 6 setOn: () => void; 7 setOff: () => void; 8 }) => React.ReactNode; 9} 10 11function Toggle({ initialValue = false, children }: ToggleProps) { 12 const [on, setOn] = useState(initialValue); 13 14 const toggle = useCallback(() => setOn((prev) => !prev), []); 15 const setOnTrue = useCallback(() => setOn(true), []); 16 const setOnFalse = useCallback(() => setOn(false), []); 17 18 return ( 19 <> 20 {children({ 21 on, 22 toggle, 23 setOn: setOnTrue, 24 setOff: setOnFalse, 25 })} 26 </> 27 ); 28} 29 30// Usage 31function App() { 32 return ( 33 <Toggle> 34 {({ on, toggle }) => ( 35 <div> 36 <button onClick={toggle}> 37 {on ? 'Hide' : 'Show'} Content 38 </button> 39 {on && <div>This is the content!</div>} 40 </div> 41 )} 42 </Toggle> 43 ); 44}

Form Handling#

1interface FormState<T> { 2 values: T; 3 errors: Partial<Record<keyof T, string>>; 4 touched: Partial<Record<keyof T, boolean>>; 5} 6 7interface FormProps<T extends Record<string, any>> { 8 initialValues: T; 9 validate?: (values: T) => Partial<Record<keyof T, string>>; 10 onSubmit: (values: T) => void | Promise<void>; 11 children: (state: { 12 values: T; 13 errors: Partial<Record<keyof T, string>>; 14 touched: Partial<Record<keyof T, boolean>>; 15 isSubmitting: boolean; 16 handleChange: (name: keyof T) => (e: React.ChangeEvent<HTMLInputElement>) => void; 17 handleBlur: (name: keyof T) => () => void; 18 handleSubmit: (e: React.FormEvent) => void; 19 }) => React.ReactNode; 20} 21 22function Form<T extends Record<string, any>>({ 23 initialValues, 24 validate, 25 onSubmit, 26 children, 27}: FormProps<T>) { 28 const [values, setValues] = useState<T>(initialValues); 29 const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); 30 const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({}); 31 const [isSubmitting, setIsSubmitting] = useState(false); 32 33 const handleChange = (name: keyof T) => ( 34 e: React.ChangeEvent<HTMLInputElement> 35 ) => { 36 const value = e.target.value; 37 setValues((prev) => ({ ...prev, [name]: value })); 38 }; 39 40 const handleBlur = (name: keyof T) => () => { 41 setTouched((prev) => ({ ...prev, [name]: true })); 42 if (validate) { 43 setErrors(validate(values)); 44 } 45 }; 46 47 const handleSubmit = async (e: React.FormEvent) => { 48 e.preventDefault(); 49 50 if (validate) { 51 const validationErrors = validate(values); 52 setErrors(validationErrors); 53 if (Object.keys(validationErrors).length > 0) return; 54 } 55 56 setIsSubmitting(true); 57 try { 58 await onSubmit(values); 59 } finally { 60 setIsSubmitting(false); 61 } 62 }; 63 64 return ( 65 <> 66 {children({ 67 values, 68 errors, 69 touched, 70 isSubmitting, 71 handleChange, 72 handleBlur, 73 handleSubmit, 74 })} 75 </> 76 ); 77} 78 79// Usage 80function LoginForm() { 81 return ( 82 <Form 83 initialValues={{ email: '', password: '' }} 84 validate={(values) => { 85 const errors: Record<string, string> = {}; 86 if (!values.email) errors.email = 'Required'; 87 if (!values.password) errors.password = 'Required'; 88 return errors; 89 }} 90 onSubmit={async (values) => { 91 await login(values); 92 }} 93 > 94 {({ values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit }) => ( 95 <form onSubmit={handleSubmit}> 96 <input 97 value={values.email} 98 onChange={handleChange('email')} 99 onBlur={handleBlur('email')} 100 /> 101 {touched.email && errors.email && <span>{errors.email}</span>} 102 103 <input 104 type="password" 105 value={values.password} 106 onChange={handleChange('password')} 107 onBlur={handleBlur('password')} 108 /> 109 {touched.password && errors.password && <span>{errors.password}</span>} 110 111 <button type="submit" disabled={isSubmitting}> 112 {isSubmitting ? 'Logging in...' : 'Log in'} 113 </button> 114 </form> 115 )} 116 </Form> 117 ); 118}

Window Size Tracker#

1interface WindowSize { 2 width: number; 3 height: number; 4} 5 6interface WindowSizeProps { 7 children: (size: WindowSize) => React.ReactNode; 8} 9 10function WindowSize({ children }: WindowSizeProps) { 11 const [size, setSize] = useState<WindowSize>({ 12 width: window.innerWidth, 13 height: window.innerHeight, 14 }); 15 16 useEffect(() => { 17 const handleResize = () => { 18 setSize({ 19 width: window.innerWidth, 20 height: window.innerHeight, 21 }); 22 }; 23 24 window.addEventListener('resize', handleResize); 25 return () => window.removeEventListener('resize', handleResize); 26 }, []); 27 28 return <>{children(size)}</>; 29} 30 31// Usage with responsive rendering 32function ResponsiveLayout() { 33 return ( 34 <WindowSize> 35 {({ width }) => ( 36 width < 768 ? <MobileLayout /> : <DesktopLayout /> 37 )} 38 </WindowSize> 39 ); 40}

Combining Multiple Render Props#

1// Compose multiple render prop components 2function Dashboard() { 3 return ( 4 <Auth> 5 {({ user }) => ( 6 <Theme> 7 {({ theme }) => ( 8 <Fetch<DashboardData> url="/api/dashboard"> 9 {({ data, loading }) => ( 10 <div className={theme}> 11 <header>Welcome, {user.name}</header> 12 {loading ? ( 13 <Spinner /> 14 ) : ( 15 <DashboardContent data={data} /> 16 )} 17 </div> 18 )} 19 </Fetch> 20 )} 21 </Theme> 22 )} 23 </Auth> 24 ); 25} 26 27// Using a compose helper 28function Compose({ components, children }: { 29 components: Array<React.ComponentType<{ children: (value: any) => React.ReactNode }>>; 30 children: (...values: any[]) => React.ReactNode; 31}) { 32 return components.reduceRight( 33 (acc, Component) => (values: any[]) => ( 34 <Component> 35 {(value) => acc([...values, value])} 36 </Component> 37 ), 38 (values: any[]) => children(...values) 39 )([]); 40} 41 42// Usage 43function Dashboard() { 44 return ( 45 <Compose components={[Auth, Theme, WindowSize]}> 46 {(user, theme, windowSize) => ( 47 <div className={theme}> 48 <span>User: {user.name}</span> 49 <span>Window: {windowSize.width}x{windowSize.height}</span> 50 </div> 51 )} 52 </Compose> 53 ); 54}

Converting to Custom Hooks#

1// Render prop to hook 2function useMousePosition() { 3 const [position, setPosition] = useState({ x: 0, y: 0 }); 4 5 useEffect(() => { 6 const handleMouseMove = (e: MouseEvent) => { 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 position; 15} 16 17// Hook-based component using render prop 18function MouseTracker({ children }: { 19 children: (position: MousePosition) => React.ReactNode; 20}) { 21 const position = useMousePosition(); 22 return <>{children(position)}</>; 23} 24 25// Now users can choose either approach 26function UsingHook() { 27 const { x, y } = useMousePosition(); 28 return <div>Mouse: {x}, {y}</div>; 29} 30 31function UsingRenderProp() { 32 return ( 33 <MouseTracker> 34 {({ x, y }) => <div>Mouse: {x}, {y}</div>} 35 </MouseTracker> 36 ); 37}

Performance Optimization#

1// Memoize the render prop callback 2function OptimizedParent() { 3 const renderContent = useCallback( 4 ({ x, y }: MousePosition) => ( 5 <div>Mouse: {x}, {y}</div> 6 ), 7 [] 8 ); 9 10 return <MouseTracker render={renderContent} />; 11} 12 13// Or memoize children in the render prop component 14function MemoizedMouseTracker({ children }: MouseTrackerProps) { 15 const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 }); 16 const childRef = useRef(children); 17 childRef.current = children; 18 19 // ... event handling 20 21 return useMemo( 22 () => childRef.current(position), 23 [position] 24 ); 25}

Best Practices#

Design: ✓ Keep render prop components focused ✓ Use TypeScript for type safety ✓ Provide sensible defaults ✓ Consider converting to hooks Performance: ✓ Memoize render prop callbacks ✓ Avoid inline functions in render ✓ Keep state updates minimal ✓ Use useMemo for expensive renders Patterns: ✓ Use children as function for cleaner JSX ✓ Export both hook and component ✓ Compose multiple render props ✓ Handle loading and error states Avoid: ✗ Deeply nested render props ✗ Over-complicated state ✗ Missing error boundaries ✗ Ignoring performance

Conclusion#

Render props enable flexible component composition by passing a function that receives data and returns JSX. While hooks have replaced many render prop use cases, the pattern remains valuable for certain scenarios. Consider providing both hook and render prop APIs for maximum flexibility.

Share this article

Help spread the word about Bootspring