What is Render props?
Render props: A render prop is a prop whose value is a function returning React elements, letting a component hand data or state to the caller, who decides the markup.
A render prop is a function passed to a component that the component calls to produce part of its output. Instead of rendering a fixed row, a list component calls renderRow(item) and inserts whatever comes back. The component owns the behavior, such as virtualization, sorting or hover state, and the caller owns the markup. The pattern was the main way to share stateful logic before hooks, as in <Mouse render={({ x, y }) => ...} />, and hooks replaced it for pure logic. It remains the right tool when a component needs to render something it cannot know in advance, in a position it controls.
Where it still fits
- Per-item rendering in lists, tables and trees:
renderRow,renderItem, a column'scellfunction. - Slots with data: a chart's tooltip that receives the hovered point, an empty state that receives the active filters.
- Children as a function, the same idea written as
{(state) => ...}between the tags, which reads well when the function is the whole content. - Not for element substitution. Changing which element a part renders as, say a trigger becoming a link, is the job of the
renderprop in Base UI orasChildin Radix, covered under polymorphic components. The name overlaps; the purpose differs.
How to build one in React
import * as React from "react"
type Column<T> = { id: string; header: string; cell: (row: T) => React.ReactNode }
export function SimpleTable<T>({ rows, columns, getKey, renderEmpty }: {
rows: T[]
columns: Column<T>[]
getKey: (row: T) => React.Key
renderEmpty?: () => React.ReactNode
}) {
if (rows.length === 0) return <>{renderEmpty?.() ?? <p>No rows.</p>}</>
return (
<table>
<thead>
<tr>{columns.map((c) => <th key={c.id} scope="col">{c.header}</th>)}</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={getKey(r)}>{columns.map((c) => <td key={c.id}>{c.cell(r)}</td>)}</tr>
))}
</tbody>
</table>
)
}
// <SimpleTable rows={invoices} columns={[{ id: "amount", header: "Amount", cell: (r) => usd.format(r.amount) }]} getKey={(r) => r.id} />
The generic T flows from rows into every cell function, so the caller gets typed rows inside the render prop with no casting. That typing is a quiet advantage over passing pre-rendered elements.
Details that matter
- Inline functions are fine. A new function per render only matters if the child is memoized; then wrap the render prop in
useCallback. - Do not call hooks inside a render prop. It runs inside the host component's render, so hook order would depend on data. Move hooks into a small component and render that instead.
- Keep render props pure. They are called during render, so no side effects and no state updates.
- Prefer a hook for logic with no markup.
useMouse()beats<Mouse render>when the caller renders everything anyway.
MiniDev's DataTable takes typed column definitions with cell functions, VirtualList takes renderRow, and VirtualizedList takes renderItem, each keeping the behavior in the component and the row markup with you. The same split powers headless components, where the library owns behavior and you own every element.