Render Props — Comparte comportamiento entre componentes pasando una función que devuelve JSX
Receta
// Un componente headless que gestiona la posición del ratón
function MouseTracker({ render }: {
render: (position: { x: number; y: number }) => React.ReactNode;
}) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return <div onMouseMove={handleMouseMove}>{render(position)}</div>;
}
// Uso
<MouseTracker
render={({ x, y }) => <p>El ratón está en ({x}, {y})</p>}
/>Cuándo usarlo: Cuando un componente encapsula comportamiento reutilizable o lógica de estado y el consumidor necesita controlar qué se renderiza. También es útil para componentes de interfaz headless.
Ejemplo Funcional
import { useState, useRef, useEffect, type ReactNode } from "react";
// Componente headless disclosure usando render prop
interface DisclosureRenderProps {
isOpen: boolean;
toggle: () => void;
open: () => void;
close: () => void;
triggerProps: {
onClick: () => void;
"aria-expanded": boolean;
"aria-controls": string;
};
contentProps: {
id: string;
role: "region";
hidden: boolean;
};
}
function Disclosure({
id,
defaultOpen = false,
children,
}: {
id: string;
defaultOpen?: boolean;
children: (props: DisclosureRenderProps) => ReactNode;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const contentId = `${id}-content`;
const api: DisclosureRenderProps = {
isOpen,
toggle: () => setIsOpen((prev) => !prev),
open: () => setIsOpen(true),
close: () => setIsOpen(false),
triggerProps: {
onClick: () => setIsOpen((prev) => !prev),
"aria-expanded": isOpen,
"aria-controls": contentId,
},
contentProps: {
id: contentId,
role: "region",
hidden: !isOpen,
},
};
return <>{children(api)}</>;
}
// Uso — control completo sobre el renderizado
function FAQ({ items }: { items: { q: string; a: string }[] }) {
return (
<div className="space-y-2">
{items.map((item, i) => (
<Disclosure key={i} id={`faq-${i}`}>
{({ triggerProps, contentProps, isOpen }) => (
<div className="border rounded-lg">
<button
{...triggerProps}
className="w-full text-left p-4 font-medium flex justify-between"
>
{item.q}
<span>{isOpen ? "−" : "+"}</span>
</button>
<div {...contentProps} className="px-4 pb-4 text-gray-600">
{item.a}
</div>
</div>
)}
</Disclosure>
))}
</div>
);
}Lo que esto demuestra:
- Patrón function-as-children (la variante de render prop más común)
- Componente headless — gestiona estado y ARIA pero no renderiza nada por sí solo
- El consumidor obtiene control total sobre la estructura del DOM y el estilo
- Los props de accesibilidad son proporcionados por el componente headless
Inmersión Profunda
Cómo Funciona
- Un render prop es cualquier prop cuyo valor es una función que devuelve
ReactNode. - El componente llama esta función durante el renderizado, pasando estado interno o valores calculados como argumentos.
- La variante
children(children: (props) => ReactNode) es la forma más común, a veces llamada "function-as-children". - El patrón separa el comportamiento (gestión de estado, manejo de eventos, accesibilidad) de la presentación (DOM, estilos).
- Las librerías de UI headless (Headless UI, primitivas de Radix, Downshift) se construyen sobre este concepto.
Parámetros y Valores de Retorno
| Parámetro | Tipo | Propósito |
|---|---|---|
render o children | (state: T) => ReactNode | Función que el consumidor proporciona para controlar el renderizado |
| Estado interno | Varía | Pasado como argumento a la función de renderizado |
| Manejadores de eventos | () => void etc. | Proporcionados al consumidor para vincularse a sus propios elementos |
| Props ARIA | Objetos de expansión | Atributos de accesibilidad pre-construidos para elementos del consumidor |
Variaciones
Render prop con nombre — útil cuando necesitas múltiples ranuras de renderizado:
function DataTable<T>({
data,
renderHeader,
renderRow,
renderEmpty,
}: {
data: T[];
renderHeader: () => ReactNode;
renderRow: (item: T, index: number) => ReactNode;
renderEmpty: () => ReactNode;
}) {
if (data.length === 0) return <>{renderEmpty()}</>;
return (
<table>
<thead>{renderHeader()}</thead>
<tbody>{data.map((item, i) => renderRow(item, i))}</tbody>
</table>
);
}Patrón de colección de props — agrupa props relacionados en objetos que se puedan expandir:
// En lugar de props individuales:
// onClick={toggle} aria-expanded={isOpen} aria-controls={id}
// Proporciona una colección:
// {...triggerProps}Notas de TypeScript
- Siempre tipifica explícitamente el argumento de la función de renderizado para que los consumidores obtengan autocompletado.
- Usa genéricos para render props que funcionan con tipos de datos variados:
children: (item: T) => ReactNode. - Exporta el tipo de argumento del render prop para que los consumidores puedan tipificar sus funciones de renderizado externamente si es necesario.
Trampas
-
Nueva función en cada renderizado — Los render props en línea crean una nueva función en cada renderizado, lo que puede interferir con
React.memo. Solución: Extrae la función de renderizado a una referencia estable conuseCallbacksi el componente padre está memoizado. -
Infierno de envolventes — Anidar múltiples componentes de render prop crea una indentación profunda. Solución: Extrae el comportamiento de cada componente de render prop en un custom hook. La mayoría de patrones de render prop se pueden convertir a hooks.
-
Retornar fragmentos sin claves — Cuando render props retornan listas, cada elemento necesita una clave. Solución: Asegúrate de que el consumidor retorna elementos con clave cuando renderiza listas.
-
Romper las reglas de hooks dentro de render props — No puedes llamar hooks dentro de la función de renderizado pasada a un render prop. Solución: Si se necesitan hooks, extrae un componente separado y pasa datos como props.
Alternativas
| Enfoque | Compensación |
|---|---|
| Render props | Control completo de renderizado; puede llevar a anidamiento |
| Custom hooks | API más limpia; no puede encapsular estructura JSX |
| Composición (ranuras) | Más simple; el contenido de ranura no puede acceder al estado interno |
| Componentes de orden superior | Agrega comportamiento de forma transparente; más difícil de tipificar correctamente |
| Componentes compuestos | Múltiples elementos relacionados comparten estado implícitamente |
Preguntas Frecuentes
¿Cuál es la diferencia entre un render prop y function-as-children?
- Un render prop es cualquier prop cuyo valor es una función que devuelve
ReactNode. - Function-as-children es una variante específica donde
childrenes la función de renderizado:children: (props) => ReactNode. - Function-as-children es la forma más común y evita el nombre de prop adicional.
¿Qué es un componente headless?
- Un componente headless gestiona estado, manejo de eventos y accesibilidad pero no renderiza DOM propio.
- El consumidor controla todo el renderizado a través de un render prop.
- Librerías como Headless UI, Radix y Downshift se construyen sobre este concepto.
¿Cómo simplifica el patrón de colección de props los render props?
// En lugar de pasar props individuales:
// onClick={toggle} aria-expanded={isOpen} aria-controls={id}
// Proporciona un objeto que se pueda expandir:
// {...triggerProps}- Agrupa props relacionados (manejadores de eventos, atributos ARIA) en un único objeto.
- Los consumidores expanden el objeto sobre su elemento, reduciendo el código repetitivo y previniendo atributos perdidos.
¿Cuándo deberías usar múltiples render props con nombre en lugar de un único children render prop?
<DataTable
data={rows}
renderHeader={() => <tr><th>Nombre</th></tr>}
renderRow={(item) => <tr><td>{item.name}</td></tr>}
renderEmpty={() => <p>Sin datos</p>}
/>- Usa múltiples render props con nombre cuando el componente tiene ranuras de renderizado distintas (encabezado, fila, estado vacío).
- Un único render prop
childrenfunciona cuando solo hay un área de renderizado.
Trampa: ¿Por qué los render props en línea rompen React.memo?
- Las funciones de renderizado en línea crean una nueva referencia de función en cada renderizado.
- Si el componente padre está memoizado, la nueva referencia hace que se re-renderice independientemente.
- Solución: extrae la función de renderizado y estabilízala con
useCallbacksi el componente padre está memoizado.
Trampa: ¿Puedes llamar hooks dentro de una función de render prop?
- No. Los hooks no pueden ser llamados dentro de la función de renderizado pasada a un render prop.
- La función se ejecuta durante el renderizado del componente padre, no como su propio componente.
- Solución: extrae un componente separado que reciba los datos como props y llama hooks dentro de ese componente.
¿Cómo tipificas un render prop genérico en TypeScript?
interface ListProps<T> {
items: T[];
children: (item: T, index: number) => ReactNode;
}
function List<T>({ items, children }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{children(item, i)}</li>)}</ul>;
}- Usa un parámetro de tipo genérico tanto en el componente como en la función de renderizado.
- TypeScript infiere
Tdel arrayitemsy lo aplica en el argumento de la función de renderizado.
¿Deberías exportar el tipo de argumento del render prop para los consumidores?
- Sí. Exportar el tipo (por ejemplo,
DisclosureRenderProps) permite que los consumidores tipifiquen sus funciones de renderizado externamente. - Esto mejora el autocompletado y detecta errores de tipo cuando la función de renderizado se define en un archivo separado.
¿Por qué son menos comunes los render props en React moderno, y qué los reemplazó?
- Los custom hooks pueden encapsular el mismo comportamiento reutilizable sin anidamiento.
- Los hooks proporcionan una API más limpia y evitan la indentación profunda ("infierno de envolventes") de render props anidados.
- Los render props siguen siendo útiles cuando necesitas encapsular estructura JSX, no solo lógica.
¿Cómo manejan los render props la accesibilidad en comparación con la composición?
- Los render props pueden proporcionar objetos de atributos ARIA pre-construidos (como
triggerPropsycontentProps) para que el consumidor los expanda. - El componente headless posee la lógica de accesibilidad mientras que el consumidor posee la estructura del DOM.
- Así es como librerías como Downshift y Headless UI proporcionan widgets accesibles sin dictar el marcado.
¿Cómo evitas el infierno de envolventes con múltiples componentes de render prop?
- Extrae el comportamiento de cada componente de render prop en un custom hook.
- Usa los hooks juntos en un único componente en lugar de anidar envolventes de render prop.
- La mayoría de patrones de render prop tienen un equivalente directo en hooks.
Relacionado
- Composition — Alternativa más simple cuando las ranuras no necesitan estado interno
- Higher-Order Components — Patrón alternativo para reutilización de comportamiento
- Compound Components — API multi-parte que comparte estado a través de context