Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rijvi-mahmud/shaddy/llms.txt

Use this file to discover all available pages before exploring further.

When two components need the same stateful behaviour but different UI, the most direct solution is to extract the logic into a component that accepts a function prop — a “render prop” — which the component calls with its state, letting the caller decide what to render. The component provides the behaviour; the caller provides the view.

The Problem

Without a sharing mechanism, logic gets duplicated. Two components that both need to track mouse position end up with identical state and effect code:
// ❌ Duplicated mouse-tracking logic in two components
function MouseTrackerA() {
  const [position, setPosition] = useState({ x: 0, y: 0 })

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

  return <div>The mouse is at ({position.x}, {position.y})</div>
}

function MouseTrackerB() {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  // ... same effect, different render
  return <img src="cursor.png" style={{ left: position.x, top: position.y }} />
}

The Solution

Extract the tracking logic into a component with a render prop. The component manages state; the prop function decides what to display:
// components/MouseTracker.tsx
import { useState, useEffect, ReactNode } from 'react'

interface MousePosition {
  x: number
  y: number
}

interface MouseTrackerProps {
  render: (position: MousePosition) => ReactNode
}

export function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 })

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

  return <>{render(position)}</>
}
Both consumers now share the same logic without duplication:
import { MouseTracker } from './components/MouseTracker'

export function App() {
  return (
    <div className="p-8">
      {/* Text display */}
      <MouseTracker
        render={({ x, y }) => (
          <p className="mb-4">
            The mouse is at <strong>({x}, {y})</strong>
          </p>
        )}
      />

      {/* Visual cursor follower */}
      <MouseTracker
        render={({ x, y }) => (
          <div
            className="fixed w-4 h-4 bg-blue-500 rounded-full pointer-events-none"
            style={{ left: x - 8, top: y - 8 }}
          />
        )}
      />
    </div>
  )
}

Realistic Example: Generic Data Fetcher

A data-fetching component that exposes loading, error, and refetch state to callers:
// components/DataFetcher.tsx
import { useState, useEffect, ReactNode } from 'react'

interface FetchState<T> {
  data: T | null
  loading: boolean
  error: Error | null
  refetch: () => void
}

interface DataFetcherProps<T> {
  url: string
  children: (state: FetchState<T>) => ReactNode
  options?: RequestInit
}

export function DataFetcher<T>({ url, children, options }: DataFetcherProps<T>) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)
  const [refetchTrigger, setRefetchTrigger] = useState(0)

  const refetch = () => setRefetchTrigger((prev) => prev + 1)

  useEffect(() => {
    let cancelled = false

    const fetchData = async () => {
      try {
        setLoading(true)
        setError(null)
        const response = await fetch(url, options)
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
        const json = await response.json()
        if (!cancelled) setData(json)
      } catch (err) {
        if (!cancelled) setError(err instanceof Error ? err : new Error('An error occurred'))
      } finally {
        if (!cancelled) setLoading(false)
      }
    }

    fetchData()
    return () => { cancelled = true }
  }, [url, refetchTrigger])

  return <>{children({ data, loading, error, refetch })}</>
}
Using DataFetcher with children as the render prop:
import { DataFetcher } from './components/DataFetcher'

interface User {
  id: number
  name: string
  email: string
  avatar: string
}

export function UserProfile({ userId }: { userId: number }) {
  return (
    <DataFetcher<User> url={`/api/users/${userId}`}>
      {({ data, loading, error, refetch }) => {
        if (loading) {
          return (
            <div className="flex items-center justify-center p-8">
              <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
            </div>
          )
        }

        if (error) {
          return (
            <div className="p-4 bg-red-50 border border-red-200 rounded">
              <p className="text-red-800">Error: {error.message}</p>
              <button onClick={refetch} className="mt-2 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
                Retry
              </button>
            </div>
          )
        }

        if (!data) return <p>No user found</p>

        return (
          <div className="bg-white shadow rounded-lg p-6">
            <div className="flex items-center gap-4">
              <img src={data.avatar} alt={data.name} className="w-16 h-16 rounded-full" />
              <div>
                <h2 className="text-xl font-bold">{data.name}</h2>
                <p className="text-gray-600">{data.email}</p>
              </div>
            </div>
            <button onClick={refetch} className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
              Refresh
            </button>
          </div>
        )
      }}
    </DataFetcher>
  )
}

Render Props vs Custom Hooks

Modern React prefers custom hooks for sharing logic, but render props remain relevant when the shared component needs to control what renders or when you’re building a library with a declarative JSX API.
// Render Props approach
<MouseTracker>
  {({ x, y }) => <div>Mouse at ({x}, {y})</div>}
</MouseTracker>

// Custom Hook approach
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const handler = (e: MouseEvent) => setPosition({ x: e.clientX, y: e.clientY })
    window.addEventListener('mousemove', handler)
    return () => window.removeEventListener('mousemove', handler)
  }, [])
  return position
}

function App() {
  const { x, y } = useMousePosition()
  return <div>Mouse at ({x}, {y})</div>
}
Render PropsCustom Hook
Control over rendering✅ Caller decides what renders❌ Hook returns data only
JSX composability✅ Fits naturally in component trees
SimplicityMore nesting in JSXCleaner component bodies
Best forComponent libraries, declarative APIsApplication-level logic sharing

Pattern Variations

Named render prop:
<DataFetcher url="/api/users" render={({ data, loading }) => <UserList users={data} />} />
Children as function (most common):
<DataFetcher url="/api/users">
  {({ data, loading }) => <UserList users={data} />}
</DataFetcher>
Multiple render props (slots):
<Component
  renderHeader={(data) => <Header {...data} />}
  renderBody={(data) => <Body {...data} />}
  renderFooter={(data) => <Footer {...data} />}
/>

When to Use

  • Multiple components need the same stateful logic but render different UI.
  • You want callers to have complete rendering control based on shared state.
  • You’re building a reusable utility component (data fetching, event tracking, etc.).
  • You want to avoid HOC wrapper components in the tree.
  • Type safety and explicit prop contracts are important.

When Not to Use

  • The logic is simple enough for a custom hook — hooks are usually cleaner.
  • The component doesn’t manage any state worth sharing.
  • The render function grows complex enough to create “callback hell” from nesting.
  • Performance is critical and creating new inline functions on each render is a concern.
Use useCallback to stabilise render prop functions when the component they target uses React.memo, preventing unnecessary re-renders caused by new function references on each parent render.

Build docs developers (and LLMs) love