Skip
Arish's avatar

31. Render Props Pattern


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!