Render Props
HardRender 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