Render Props

Hard

Render props is a pattern where a component accepts a function that returns JSX, calling it with internal state or logic. This inverts control so the logic-owning component does not own the UI. While largely replaced by custom hooks, render props remain useful when the shared logic involves JSX structure.

Interactive Visualization

Key Points

  • A function prop (often children) receives internal state and returns JSX
  • Inverts control: logic component provides data, consumer provides UI
  • Largely replaced by custom hooks for sharing stateful logic
  • Still useful when the shared logic involves DOM structure or wrapper elements
  • Can cause unnecessary nesting if overused
  • TypeScript can fully type the render function parameters

Code Examples

Mouse Tracker Render Prop

import { useState, type ReactNode } from 'react'

interface MousePosition {
  x: number
  y: number
}

interface MouseTrackerProps {
  children: (pos: MousePosition) => ReactNode
}

function MouseTracker({ children }: MouseTrackerProps) {
  const [pos, setPos] = useState<MousePosition>({ x: 0, y: 0 })

  const handleMouseMove = (e: React.MouseEvent) => {
    setPos({ x: e.clientX, y: e.clientY })
  }

  return (
    <div onMouseMove={handleMouseMove} style={{ height: '100%' }}>
      {children(pos)}
    </div>
  )
}

function App() {
  return (
    <MouseTracker>
      {({ x, y }) => (
        <div>
          <p>Cursor at ({x}, {y})</p>
          <div
            style={{
              position: 'absolute',
              left: x - 10,
              top: y - 10,
              width: 20,
              height: 20,
              borderRadius: '50%',
              background: 'blue',
            }}
          />
        </div>
      )}
    </MouseTracker>
  )
}

MouseTracker manages the mouse position state while the consumer decides how to render it through the children function

Render Prop vs Custom Hook

import { useState, useEffect } from 'react'

// Render prop approach
interface FetchRenderProps<T> {
  url: string
  children: (state: { data: T | null; loading: boolean }) => ReactNode
}

function Fetch<T>({ url, children }: FetchRenderProps<T>) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch(url)
      .then((r) => r.json() as Promise<T>)
      .then((d) => { setData(d); setLoading(false) })
  }, [url])

  return <>{children({ data, loading })}</>
}

// Custom hook approach (usually preferred)
function useFetch<T>(url: string): { data: T | null; loading: boolean } {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch(url)
      .then((r) => r.json() as Promise<T>)
      .then((d) => { setData(d); setLoading(false) })
  }, [url])

  return { data, loading }
}

Custom hooks are generally cleaner for sharing logic, but render props are still useful when the shared code needs to render wrapper elements

Common Mistakes

  • Creating deeply nested render prop callbacks that are hard to read
  • Using render props when a custom hook would be simpler and cleaner
  • Not typing the render function parameters properly in TypeScript

Interview Tips

  • Explain the historical context: render props solved the same problem custom hooks do now
  • Know when render props are still useful over custom hooks (when JSX structure is shared)
  • Be able to convert a render prop component into a custom hook

Related Concepts