Render Props
Render props pattern for sharing cross-cutting logic through function-as-children and render callbacks
You are an expert in the Render Props pattern for building React applications. ## Key Points - **Function as Children**: The most common variant — `children` is a function that receives data and returns JSX. - **Render Callback Prop**: A named prop (e.g., `render`, `renderItem`) that serves the same purpose. - **Separation of Concerns**: Logic lives inside the provider component; presentation lives in the consumer's render function. - **Composability**: Multiple render-prop components can be nested to combine behaviors. - Type the render function's parameters explicitly so consumers get full IntelliSense. - Use `children` as the render prop when the component has a single render callback; use named props (`renderItem`, `renderHeader`) when there are multiple. - Memoize data passed to the render function if it is an object or array created on each render. - Consider extracting the logic into a custom hook if the component does not need to control the DOM wrapper. - Document the shape of the arguments the render function receives. - **Inline function re-creation**: Defining the render function inline on every render can cause unnecessary re-renders in children. Extract it or use `useCallback` when performance matters. - **Callback hell**: Deeply nesting render props becomes unreadable. Flatten by composing with custom hooks or by breaking the tree into smaller components. - **Ignoring `key` props**: When render props produce lists, the consumer must provide stable keys.
skilldb get react-patterns-skills/Render PropsFull skill: 181 linesRender Props — React Patterns
You are an expert in the Render Props pattern for building React applications.
Overview
Render props is a technique where a component receives a function as a prop (or as children) and calls that function to determine what to render. The function receives internal state or behavior as arguments, letting consumers control rendering while the component owns the logic. Although custom hooks have replaced many render-prop use cases, the pattern remains valuable for components that need to share logic while controlling the rendering lifecycle.
Core Philosophy
Render props solve the tension between reusable logic and custom presentation. A component that tracks mouse position, manages a dropdown's open/close state, or handles virtualized scrolling contains valuable logic, but the moment it dictates how that logic is rendered, it becomes rigid. Render props let the logic-owning component hand control of the visual output to the consumer, creating a clean separation between "what it does" and "what it looks like."
This pattern predates hooks and was the primary way to share cross-cutting behavior in class component codebases. While custom hooks have absorbed many of those use cases, render props remain the right choice when the logic provider needs to control the DOM wrapper (e.g., attaching event listeners to a specific element) or when you want to expose a component-level API that participates in React's rendering lifecycle rather than being called imperatively. Render props are also the natural API for components like virtualized lists and drag-and-drop containers that need to own a DOM node while letting consumers define what goes inside.
The guiding principle is explicitness. The render function's signature documents exactly what data and callbacks the consumer receives. Unlike higher-order components, which inject props invisibly and create naming collisions, render props make the contract visible at the call site. Every piece of shared state is passed as a named argument, and the consumer decides what to do with it.
Core Concepts
- Function as Children: The most common variant —
childrenis a function that receives data and returns JSX. - Render Callback Prop: A named prop (e.g.,
render,renderItem) that serves the same purpose. - Separation of Concerns: Logic lives inside the provider component; presentation lives in the consumer's render function.
- Composability: Multiple render-prop components can be nested to combine behaviors.
Implementation Patterns
Mouse Tracker
import { useState, ReactNode } from "react";
interface Position {
x: number;
y: number;
}
interface MouseTrackerProps {
children: (position: Position) => ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
return (
<div
style={{ height: "100%" }}
onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
>
{children(position)}
</div>
);
}
// Usage
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<p>
Mouse is at ({x}, {y})
</p>
)}
</MouseTracker>
);
}
Fetch Component with Render Prop
import { useState, useEffect, ReactNode } from "react";
interface FetchProps<T> {
url: string;
children: (state: { data: T | null; loading: boolean; error: Error | null }) => ReactNode;
}
function Fetch<T>({ url, children }: FetchProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((json) => {
if (!cancelled) {
setData(json);
setLoading(false);
}
})
.catch((err) => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [url]);
return <>{children({ data, loading, error })}</>;
}
// Usage
<Fetch<User[]> url="/api/users">
{({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserList users={data!} />;
}}
</Fetch>
Named Render Props for Lists
interface VirtualListProps<T> {
items: T[];
itemHeight: number;
renderItem: (item: T, index: number) => ReactNode;
renderEmpty?: () => ReactNode;
}
function VirtualList<T>({ items, itemHeight, renderItem, renderEmpty }: VirtualListProps<T>) {
if (items.length === 0 && renderEmpty) {
return <>{renderEmpty()}</>;
}
return (
<div style={{ height: items.length * itemHeight, position: "relative" }}>
{items.map((item, index) => (
<div
key={index}
style={{ position: "absolute", top: index * itemHeight, height: itemHeight }}
>
{renderItem(item, index)}
</div>
))}
</div>
);
}
Best Practices
- Type the render function's parameters explicitly so consumers get full IntelliSense.
- Use
childrenas the render prop when the component has a single render callback; use named props (renderItem,renderHeader) when there are multiple. - Memoize data passed to the render function if it is an object or array created on each render.
- Consider extracting the logic into a custom hook if the component does not need to control the DOM wrapper.
- Document the shape of the arguments the render function receives.
Common Pitfalls
- Inline function re-creation: Defining the render function inline on every render can cause unnecessary re-renders in children. Extract it or use
useCallbackwhen performance matters. - Callback hell: Deeply nesting render props becomes unreadable. Flatten by composing with custom hooks or by breaking the tree into smaller components.
- Ignoring
keyprops: When render props produce lists, the consumer must provide stable keys. - Mixing patterns: Avoid accepting both
childrenas a render function andchildrenas regular JSX — pick one API and be explicit.
Anti-Patterns
-
Render prop where a hook would suffice: Using a render-prop component solely to share stateful logic when the component does not need to own a DOM node. If the provider renders nothing but
<>{children(data)}</>, a custom hook would be simpler, avoid an extra component in the tree, and eliminate the nesting. -
Deeply nested render-prop "pyramids": Composing three or more render-prop components inline, creating callback hell that is impossible to read or debug. Flatten by extracting inner render functions into named components or by combining the logic into a single custom hook.
-
Unstable render function references: Passing an arrow function as the render prop on every render without memoization. When the render-prop component uses
React.memoorshouldComponentUpdate, the new function reference defeats the optimization and causes unnecessary re-renders of the entire subtree. -
Overloading the render function's arguments: Passing ten or more arguments to the render callback, turning the call site into a destructuring exercise. Keep the argument surface small and cohesive; if consumers need different slices of data, provide multiple named render props (e.g.,
renderHeader,renderItem,renderEmpty). -
Dual-mode children: Accepting
childrenas both a render function and regular ReactNode, then branching internally withtypeof children === "function". This creates an ambiguous API where TypeScript cannot enforce the correct usage, and consumers cannot tell from the call site which mode is active.
Install this skill directly: skilldb add react-patterns-skills
Related Skills
Compound Components
Compound component pattern for building flexible, implicitly-shared React component APIs
Context Patterns
React Context patterns for efficient state sharing, provider composition, and avoiding unnecessary re-renders
Custom Hooks
Custom hooks pattern for extracting and reusing stateful logic across React components
Error Boundaries
Error boundary pattern for gracefully catching and recovering from runtime errors in React component trees
Optimistic Updates
Optimistic update patterns for instant UI feedback with server reconciliation and rollback in React
Server Components
React Server Components pattern for zero-bundle server-rendered components with direct backend access