Skip to main content

Render Props Pattern

What Are Render Props?

A component that accepts a function as a prop and calls it to determine what to render — giving the consumer complete control over rendering while the provider supplies data and behavior.

// Provider: supplies mouse position
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
{render(pos)} {/* Consumer decides what to render */}
</div>
);
}

// Consumer: decides what to render
<MouseTracker render={({ x, y }) => (
<h1>Mouse at {x}, {y}</h1>
)} />

Three Implementation Styles

// 1. Named "render" prop
<DataProvider render={(data) => <Display data={data} />} />

// 2. Children as function (most common)
<DataProvider>
{(data) => <Display data={data} />}
</DataProvider>

// 3. Any named prop
<DataProvider content={(data) => <Display data={data} />} />

Real-World Examples

Reusable Data Fetcher

function DataFetcher({ url, children }) {
const { data, loading, error } = useFetch(url);
return children({ data, loading, error });
}

<DataFetcher url="/api/products">
{({ data: products, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <ProductGrid products={products} />;
}}
</DataFetcher>

Intersection Observer (Lazy Loading)

function IntersectionObserver({ children, threshold = 0.1 }) {
const [isVisible, setVisible] = useState(false);
const ref = useRef(null);

useEffect(() => {
const observer = new window.IntersectionObserver(
([entry]) => setVisible(entry.isIntersecting),
{ threshold }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, [threshold]);

return <div ref={ref}>{children({ isVisible, ref })}</div>;
}

<IntersectionObserver>
{({ isVisible }) => isVisible ? <HeavyChart /> : <Placeholder />}
</IntersectionObserver>

Performance Optimization

Inline functions create new references every render — breaking memoization:

// ❌ New function every render — breaks React.memo in children
<Provider render={(data) => <Display data={data} />} />

// ✅ Stable reference
const renderDisplay = useCallback(
(data) => <Display data={data} />,
[]
);
<Provider render={renderDisplay} />

Render Props vs Hooks vs HOCs

ScenarioBest Choice
Library APIs needing flexible renderingRender Props
Application logic in function componentsCustom Hooks
Legacy/class component enhancementHOC or Render Props
Animation/virtualization needing render controlRender Props
Data fetching in app codeCustom Hooks
Cross-cutting concerns (analytics, feature flags)HOC or Hooks

2024 default: Hooks for almost everything. Render props for component libraries where the API needs to be flexible about what gets rendered.

Common Mistakes

// ❌ Creating new component type inside render prop
<Provider render={(data) => {
function InlineComponent() { return <div>{data}</div>; }
return <InlineComponent />; // New component type each render!
}} />

// ✅ Define outside or use JSX directly
<Provider render={(data) => <div>{data}</div>} />

// ❌ Deeply nested render props (callback hell)
<AuthProvider render={auth =>
<DataProvider render={data =>
<UIProvider render={ui =>
<Dashboard auth={auth} data={data} ui={ui} />
} />
} />
} />

// ✅ Use hooks or compound components instead
function Dashboard() {
const auth = useAuth();
const data = useData();
const ui = useUI();
return <DashboardContent auth={auth} data={data} ui={ui} />;
}

Content from Frontend-Master-Prep-Series03-react