Render props share code between components using a function prop. Here's how to use the pattern effectively.
Basic Render Props#
1// Component with render prop
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({ 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}
38
39// Children as render prop (common pattern)
40interface MouseTrackerChildrenProps {
41 children: (position: MousePosition) => React.ReactNode;
42}
43
44function MouseTrackerChildren({ children }: MouseTrackerChildrenProps) {
45 const [position, setPosition] = useState({ x: 0, y: 0 });
46 // ... same logic
47 return <>{children(position)}</>;
48}
49
50// Usage
51<MouseTrackerChildren>
52 {({ x, y }) => <div>Position: {x}, {y}</div>}
53</MouseTrackerChildren>Data Fetching with Render Props#
1interface FetchState<T> {
2 data: T | null;
3 loading: boolean;
4 error: Error | null;
5 refetch: () => void;
6}
7
8interface FetchProps<T> {
9 url: string;
10 children: (state: FetchState<T>) => React.ReactNode;
11}
12
13function Fetch<T>({ url, children }: FetchProps<T>) {
14 const [data, setData] = useState<T | null>(null);
15 const [loading, setLoading] = useState(true);
16 const [error, setError] = useState<Error | null>(null);
17
18 const fetchData = useCallback(async () => {
19 setLoading(true);
20 setError(null);
21
22 try {
23 const response = await fetch(url);
24 if (!response.ok) throw new Error('Fetch failed');
25 const json = await response.json();
26 setData(json);
27 } catch (err) {
28 setError(err as Error);
29 } finally {
30 setLoading(false);
31 }
32 }, [url]);
33
34 useEffect(() => {
35 fetchData();
36 }, [fetchData]);
37
38 return <>{children({ data, loading, error, refetch: fetchData })}</>;
39}
40
41// Usage
42function UserProfile({ userId }: { userId: string }) {
43 return (
44 <Fetch<User> url={`/api/users/${userId}`}>
45 {({ data, loading, error, refetch }) => {
46 if (loading) return <Spinner />;
47 if (error) return <Error message={error.message} onRetry={refetch} />;
48 if (!data) return null;
49
50 return (
51 <div>
52 <h1>{data.name}</h1>
53 <p>{data.email}</p>
54 </div>
55 );
56 }}
57 </Fetch>
58 );
59}Toggle Component#
1interface ToggleState {
2 on: boolean;
3 toggle: () => void;
4 setOn: () => void;
5 setOff: () => void;
6}
7
8interface ToggleProps {
9 initialOn?: boolean;
10 children: (state: ToggleState) => React.ReactNode;
11}
12
13function Toggle({ initialOn = false, children }: ToggleProps) {
14 const [on, setOn] = useState(initialOn);
15
16 const state: ToggleState = {
17 on,
18 toggle: () => setOn(prev => !prev),
19 setOn: () => setOn(true),
20 setOff: () => setOn(false),
21 };
22
23 return <>{children(state)}</>;
24}
25
26// Usage
27function App() {
28 return (
29 <Toggle initialOn={false}>
30 {({ on, toggle }) => (
31 <div>
32 <p>Toggle is {on ? 'ON' : 'OFF'}</p>
33 <button onClick={toggle}>Toggle</button>
34 </div>
35 )}
36 </Toggle>
37 );
38}
39
40// Multiple toggles
41function Settings() {
42 return (
43 <Toggle>
44 {({ on: darkMode, toggle: toggleDark }) => (
45 <Toggle>
46 {({ on: notifications, toggle: toggleNotifications }) => (
47 <div>
48 <label>
49 <input
50 type="checkbox"
51 checked={darkMode}
52 onChange={toggleDark}
53 />
54 Dark Mode
55 </label>
56 <label>
57 <input
58 type="checkbox"
59 checked={notifications}
60 onChange={toggleNotifications}
61 />
62 Notifications
63 </label>
64 </div>
65 )}
66 </Toggle>
67 )}
68 </Toggle>
69 );
70}Form Handling#
1interface FormState<T> {
2 values: T;
3 errors: Partial<Record<keyof T, string>>;
4 touched: Partial<Record<keyof T, boolean>>;
5 handleChange: (name: keyof T, value: any) => void;
6 handleBlur: (name: keyof T) => void;
7 handleSubmit: (e: React.FormEvent) => void;
8 isValid: boolean;
9 isSubmitting: boolean;
10}
11
12interface FormProps<T> {
13 initialValues: T;
14 validate?: (values: T) => Partial<Record<keyof T, string>>;
15 onSubmit: (values: T) => Promise<void> | void;
16 children: (state: FormState<T>) => React.ReactNode;
17}
18
19function Form<T extends Record<string, any>>({
20 initialValues,
21 validate,
22 onSubmit,
23 children,
24}: FormProps<T>) {
25 const [values, setValues] = useState<T>(initialValues);
26 const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
27 const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
28 const [isSubmitting, setIsSubmitting] = useState(false);
29
30 const handleChange = (name: keyof T, value: any) => {
31 setValues(prev => ({ ...prev, [name]: value }));
32 };
33
34 const handleBlur = (name: keyof T) => {
35 setTouched(prev => ({ ...prev, [name]: true }));
36 if (validate) {
37 setErrors(validate(values));
38 }
39 };
40
41 const handleSubmit = async (e: React.FormEvent) => {
42 e.preventDefault();
43
44 if (validate) {
45 const validationErrors = validate(values);
46 setErrors(validationErrors);
47 if (Object.keys(validationErrors).length > 0) return;
48 }
49
50 setIsSubmitting(true);
51 try {
52 await onSubmit(values);
53 } finally {
54 setIsSubmitting(false);
55 }
56 };
57
58 const isValid = Object.keys(errors).length === 0;
59
60 return (
61 <form onSubmit={handleSubmit}>
62 {children({
63 values,
64 errors,
65 touched,
66 handleChange,
67 handleBlur,
68 handleSubmit,
69 isValid,
70 isSubmitting,
71 })}
72 </form>
73 );
74}
75
76// Usage
77interface LoginValues {
78 email: string;
79 password: string;
80}
81
82function LoginForm() {
83 return (
84 <Form<LoginValues>
85 initialValues={{ email: '', password: '' }}
86 validate={(values) => {
87 const errors: Partial<Record<keyof LoginValues, string>> = {};
88 if (!values.email) errors.email = 'Required';
89 if (!values.password) errors.password = 'Required';
90 return errors;
91 }}
92 onSubmit={async (values) => {
93 await login(values);
94 }}
95 >
96 {({ values, errors, touched, handleChange, handleBlur, isSubmitting }) => (
97 <>
98 <input
99 value={values.email}
100 onChange={e => handleChange('email', e.target.value)}
101 onBlur={() => handleBlur('email')}
102 />
103 {touched.email && errors.email && <span>{errors.email}</span>}
104
105 <input
106 type="password"
107 value={values.password}
108 onChange={e => handleChange('password', e.target.value)}
109 onBlur={() => handleBlur('password')}
110 />
111 {touched.password && errors.password && <span>{errors.password}</span>}
112
113 <button type="submit" disabled={isSubmitting}>
114 {isSubmitting ? 'Logging in...' : 'Login'}
115 </button>
116 </>
117 )}
118 </Form>
119 );
120}Scroll Position#
1interface ScrollState {
2 x: number;
3 y: number;
4 direction: 'up' | 'down' | null;
5 isAtTop: boolean;
6 isAtBottom: boolean;
7}
8
9interface ScrollTrackerProps {
10 children: (state: ScrollState) => React.ReactNode;
11}
12
13function ScrollTracker({ children }: ScrollTrackerProps) {
14 const [state, setState] = useState<ScrollState>({
15 x: 0,
16 y: 0,
17 direction: null,
18 isAtTop: true,
19 isAtBottom: false,
20 });
21
22 useEffect(() => {
23 let lastY = window.scrollY;
24
25 const handleScroll = () => {
26 const y = window.scrollY;
27 const x = window.scrollX;
28 const direction = y > lastY ? 'down' : y < lastY ? 'up' : null;
29 const isAtTop = y === 0;
30 const isAtBottom =
31 y + window.innerHeight >= document.documentElement.scrollHeight;
32
33 setState({ x, y, direction, isAtTop, isAtBottom });
34 lastY = y;
35 };
36
37 window.addEventListener('scroll', handleScroll, { passive: true });
38 return () => window.removeEventListener('scroll', handleScroll);
39 }, []);
40
41 return <>{children(state)}</>;
42}
43
44// Usage: Hide header on scroll down
45function App() {
46 return (
47 <ScrollTracker>
48 {({ direction, isAtTop }) => (
49 <header
50 style={{
51 position: 'fixed',
52 transform: direction === 'down' && !isAtTop
53 ? 'translateY(-100%)'
54 : 'translateY(0)',
55 transition: 'transform 0.3s',
56 }}
57 >
58 Navigation
59 </header>
60 )}
61 </ScrollTracker>
62 );
63}Window Size#
1interface WindowSize {
2 width: number;
3 height: number;
4 isMobile: boolean;
5 isTablet: boolean;
6 isDesktop: boolean;
7}
8
9interface WindowSizeProps {
10 children: (size: WindowSize) => React.ReactNode;
11}
12
13function WindowSize({ children }: WindowSizeProps) {
14 const [size, setSize] = useState<WindowSize>({
15 width: window.innerWidth,
16 height: window.innerHeight,
17 isMobile: window.innerWidth < 768,
18 isTablet: window.innerWidth >= 768 && window.innerWidth < 1024,
19 isDesktop: window.innerWidth >= 1024,
20 });
21
22 useEffect(() => {
23 const handleResize = () => {
24 const width = window.innerWidth;
25 const height = window.innerHeight;
26
27 setSize({
28 width,
29 height,
30 isMobile: width < 768,
31 isTablet: width >= 768 && width < 1024,
32 isDesktop: width >= 1024,
33 });
34 };
35
36 window.addEventListener('resize', handleResize);
37 return () => window.removeEventListener('resize', handleResize);
38 }, []);
39
40 return <>{children(size)}</>;
41}
42
43// Usage
44<WindowSize>
45 {({ isMobile, isDesktop }) => (
46 isMobile ? <MobileNav /> : <DesktopNav />
47 )}
48</WindowSize>Render Props vs Hooks#
1// Render prop version
2function MouseTracker({ render }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 // ... event listeners
5 return render(position);
6}
7
8// Hook version (usually preferred)
9function useMousePosition() {
10 const [position, setPosition] = useState({ x: 0, y: 0 });
11 // ... event listeners
12 return position;
13}
14
15// Usage comparison
16// Render props
17<MouseTracker render={({ x, y }) => <div>{x}, {y}</div>} />
18
19// Hooks
20function Component() {
21 const { x, y } = useMousePosition();
22 return <div>{x}, {y}</div>;
23}
24
25// When render props are still useful:
26// 1. Rendering multiple children
27// 2. Third-party library integration
28// 3. Backwards compatibilityBest Practices#
Design:
✓ Use children as function when possible
✓ Provide TypeScript generics
✓ Keep render prop focused
✓ Consider hooks as alternative
Performance:
✓ Memoize render functions
✓ Avoid creating functions in render
✓ Use useCallback for handlers
✓ Extract static JSX
Naming:
✓ Use 'render' or 'children' for prop name
✓ Document expected function signature
✓ Return descriptive state objects
✓ Include helper functions in state
Conclusion#
Render props enable flexible code sharing between components. While hooks have replaced many render prop use cases, the pattern remains valuable for complex rendering logic and backwards compatibility. Use TypeScript for type safety and memoization for performance.