ReactRender Props Pattern

Render Props Pattern

A render prop is a technique for sharing behavior between components using a prop whose value is a function that returns JSX. Instead of hardcoding what to render, a component calls a function you give it and renders whatever that function returns. The component supplies the logic; you supply the UI.

The Core Idea

Imagine you want to track the mouse position and use it in several different components. Without render props you would copy the tracking logic into each component. With render props, one component owns the logic and any number of consumers can plug in their own UI.

JSX
// The "provider" component: owns mouse-tracking logic,
// delegates rendering to the caller via a render prop
function Mouse({ render }) {
  const [position, setPosition] = React.useState({ x: 0, y: 0 })

  function handleMouseMove(event) {
    setPosition({ x: event.clientX, y: event.clientY })
  }

  return (
    <div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
      {/* Call the function prop with the shared state */}
      {render(position)}
    </div>
  )
}

// Usage: consumer decides what to render
function App() {
  return (
    <Mouse
      render={({ x, y }) => (
        <p>
          Mouse is at ({x}, {y})
        </p>
      )}
    />
  )
}

The Mouse component never mentions <p> or <Cat> or any specific UI. It just tracks the mouse position and hands it to whoever is listening.

Classic Example: Mouse + Cat

The canonical render-props demo shows a cat image following the cursor. The Mouse component knows nothing about cats — it only handles events and state.

JSX
function Cat({ position }) {
  return (
    <img
      src="/cat.png"
      alt="Cat"
      style={{
        position: 'absolute',
        left: position.x - 16,
        top: position.y - 16,
        width: 32,
        height: 32,
      }}
    />
  )
}

function App() {
  return (
    <div style={{ position: 'relative' }}>
      <Mouse render={(position) => <Cat position={position} />} />
    </div>
  )
}

// Later you can reuse <Mouse> with completely different UI:
function CrosshairApp() {
  return (
    <Mouse
      render={({ x, y }) => (
        <>
          <div style={{ position: 'absolute', left: x, top: 0, width: 1, height: '100vh', background: 'red' }} />
          <div style={{ position: 'absolute', left: 0, top: y, width: '100vw', height: 1, background: 'red' }} />
        </>
      )}
    />
  )
}
Children as a Function

A common variation uses the children prop instead of a custom prop named render. This reads more naturally in JSX because you write the function between the opening and closing tags.

JSX
// Using children as a function instead of a named render prop
function Mouse({ children }) {
  const [position, setPosition] = React.useState({ x: 0, y: 0 })

  return (
    <div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
         style={{ height: '100vh' }}>
      {children(position)}
    </div>
  )
}

// Usage — the function sits between the tags, just like a normal child
function App() {
  return (
    <Mouse>
      {({ x, y }) => (
        <p>
          Cursor at ({x}, {y})
        </p>
      )}
    </Mouse>
  )
}
Note
Both styles are equivalent. Many libraries (Formik's older API, React Router's `<Route render>`, Downshift) used render props. `children as a function` is slightly more idiomatic because it avoids an extra prop name.
A Reusable Toggle Component

Render props really shine when the shared behavior involves multiple pieces of state or non-trivial logic. Here is a Toggle component that manages open/closed state and exposes it through a render prop:

JSX
function Toggle({ children }) {
  const [on, setOn] = React.useState(false)

  return children({
    on,
    toggle: () => setOn((prev) => !prev),
    setOn: () => setOn(true),
    setOff: () => setOn(false),
  })
}

// Consumer 1: a simple show/hide button
function ToggleMessage() {
  return (
    <Toggle>
      {({ on, toggle }) => (
        <div>
          <button onClick={toggle}>{on ? 'Hide' : 'Show'}</button>
          {on && <p>Hidden content revealed!</p>}
        </div>
      )}
    </Toggle>
  )
}

// Consumer 2: a switch that snaps open/closed
function ToggleSwitch() {
  return (
    <Toggle>
      {({ on, toggle }) => (
        <button
          onClick={toggle}
          style={{ background: on ? 'green' : 'gray', color: 'white' }}
        >
          {on ? 'ON' : 'OFF'}
        </button>
      )}
    </Toggle>
  )
}
The Problem Render Props Solve
  • Logic reuse without inheritance — share stateful behavior across unrelated components without class inheritance or copy-pasting

  • Consumer controls the UI — the provider handles logic; the consumer decides how to display it, keeping concerns cleanly separated

  • No implicit prop merging — unlike HOCs, you can always see exactly which data the pattern injects because it is an explicit function argument

  • Avoids "wrapper hell" — compared to early HOC chains, a single render prop component is easier to follow in a trace

Modern Alternative: Custom Hooks

Since React 16.8, most render-prop use cases can be replaced with a custom hook. Hooks compose logic without adding wrapper nodes to the component tree, and the code is often easier to read.

JSX
// The same mouse-tracking logic as a custom hook
function useMousePosition() {
  const [position, setPosition] = React.useState({ x: 0, y: 0 })

  React.useEffect(() => {
    function handleMouseMove(e) {
      setPosition({ x: e.clientX, y: e.clientY })
    }
    window.addEventListener('mousemove', handleMouseMove)
    return () => window.removeEventListener('mousemove', handleMouseMove)
  }, [])

  return position
}

// Usage: no wrapper component, no extra DOM nodes
function App() {
  const { x, y } = useMousePosition()
  return <p>Cursor at ({x}, {y})</p>
}

function Cat() {
  const { x, y } = useMousePosition()
  return (
    <img
      src="/cat.png"
      style={{ position: 'absolute', left: x - 16, top: y - 16 }}
    />
  )
}
When Render Props Are Still Useful

Scenario

Why render props still fit

Headless UI libraries (Downshift, React Table v7)

Library ships behavior; app supplies markup — natural API boundary

Complex render logic with multiple slots

Pass multiple render functions for header, body, footer separately

Wrapping third-party components that accept render props

Adapting an existing API without rewriting it

Class components that cannot use hooks

Render props work in both class and function components

Warning
Render props that return JSX cause a new function to be created on every parent render. If the child component is wrapped in React.memo, that memoization will be defeated because the render prop reference changes each time. Extract the function outside the component, or use useCallback, to avoid this.
Tip
In greenfield code, prefer custom hooks over render props for logic sharing. Save render props for cases where you need to hand rendering control back to the consumer — for instance, when building a headless or unstyled component library.