¿Qué es el Patrón Render Props?
El patrón Render Props es una forma de compartir lógica entre componentes en React. Consiste en crear un componente que acepta una prop (generalmente llamada render o directamente como children) que es una función. Esta función retorna JSX y recibe los datos o métodos que el componente envolvente quiere compartir.
Imagina que tienes un componente que maneja la posición del mouse. En lugar de copiar y pegar ese código en cada lugar donde lo necesitas, puedes crear un componente MouseTracker que haga todo el trabajo pesado y "pase" la información a través de una función.
render) le indica al componente qué debe renderizar en cada momento, basándose en el estado interno que ese componente maneja.Sintaxis Básica
Existen dos formas principales de implementar el patrón Render Props:
1. Usando la prop "render"
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
});
};
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
}
// Uso:
<MouseTracker render={mouse => (
<h1>Posición del mouse: {mouse.x}, {mouse.y}</h1>
)}/>
2. Usando children como función (más común)
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
});
};
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{this.props.children(this.state)}
</div>
);
}
}
// Uso:
<MouseTracker>
{mouse => (
<h1>Posición del mouse: {mouse.x}, {mouse.y}</h1>
)}
</MouseTracker>
children es más popular porque mantiene una sintaxis JSX más limpia y natural, sin necesidad de pasar la prop explícitamente.Ejemplo Práctico: Componente de Petición de Datos
Uno de los usos más poderosos de Render Props es crear componentes que manejen lógica asíncrona, como peticiones HTTP, y compartan el resultado con los componentes que lo necesitan.
class DataFetcher extends React.Component {
state = {
data: null,
loading: true,
error: null
};
async componentDidMount() {
try {
const response = await fetch(this.props.url);
const data = await response.json();
this.setState({ data, loading: false });
} catch (error) {
this.setState({ error, loading: false });
}
}
render() {
return this.props.children(this.state);
}
}
// Uso:
<DataFetcher url="https://api.ejemplo.com/usuarios">
{({ data, loading, error }) => {
if (loading) return <div>Cargando...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(usuario => (
<li key={usuario.id}>{usuario.nombre}</li>
))}
</ul>
);
}}
</DataFetcher>
render del componente padre. Esto puede causar re-renderizados innecesarios porque se crearía una nueva función en cada render. Usa métodos bindeados o funcionesflecha definidas fuera de render.Ejemplo: Componente de Lista con Filtros
Veamos un ejemplo más complejo que combine lógica de filtrado y renderizado:
class FilterableList extends React.Component {
state = {
filterText: '',
onlyShowFavorites: false
};
setFilterText = (text) => {
this.setState({ filterText: text });
};
toggleFavorites = () => {
this.setState(prev => ({
onlyShowFavorites: !prev.onlyShowFavorites
}));
};
getFilteredItems() {
const { filterText, onlyShowFavorites } = this.state;
return this.props.items.filter(item => {
const matchesText = item.name
.toLowerCase()
.includes(filterText.toLowerCase());
const matchesFavorite = onlyShowFavorites ? item.isFavorite : true;
return matchesText && matchesFavorite;
});
}
render() {
const props = {
items: this.getFilteredItems(),
filterText: this.state.filterText,
onlyShowFavorites: this.state.onlyShowFavorites,
setFilterText: this.setFilterText,
toggleFavorites: this.toggleFavorites
};
return this.props.children(props);
}
}
// Uso:
<FilterableList items={productos}>
{({ items, filterText, onlyShowFavorites, setFilterText, toggleFavorites }) => (
<div>
<input
type="text"
placeholder="Buscar..."
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={onlyShowFavorites}
onChange={toggleFavorites}
/>
Solo favoritos
</label>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{item.isFavorite && '⭐'}
</li>
))}
</ul>
</div>
)}
</FilterableList>
Render Props vs. Higher-Order Components (HOC)
Render Props no es el único patrón para compartir lógica. Los Higher-Order Components (HOC) son otra alternativa popular. Veamos las diferencias:
| Aspecto | Render Props | HOC |
|---|---|---|
| Estructura | Componente que recibe una función como children | Función que retorna un nuevo componente |
| Anidamiento | Se leen naturalmente en el JSX | Puede causar "wrapper hell" con muchos niveles |
| Props | Las props se pasan explícitamente | Puede haber conflictos de props (necesitas recomponer props) |
| Flexibilidad | Muy flexible para renderizado condicional | Mejor para modificar comportamiento global |
Render Props con Hooks
Con la introducción de los Hooks en React 16.8, muchos de los casos de uso de Render Props pueden resolverse de manera más elegante con hooks personalizados. Sin embargo, Render Props sigue siendo útil cuando:
- Necesitas renderizar algo basado en el estado de un componente que no quieres convertir en hook.
- Estás trabajando con librerías de terceros que ya usan este patrón.
- Quieres mantener la composición de componentes para equipos que ya conocen el patrón.
// Equivalente usando hooks:
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
// Uso:
function Componente() {
const mouse = useMousePosition();
return <h1>X: {mouse.x}, Y: {mouse.y}</h1>;
}
Ejemplo Real: Componente de Portal
Veamos cómo implementar un Render Props para un componente de Portal que renderiza contenido en otro lugar del DOM:
import { createPortal } from 'react-dom';
class Portal extends React.Component {
constructor(props) {
super(props);
this.portalRoot = document.getElementById(props.portalId || 'portal-root');
if (!this.portalRoot) {
this.portalRoot = document.createElement('div');
this.portalRoot.id = props.portalId || 'portal-root';
document.body.appendChild(this.portalRoot);
}
}
render() {
return createPortal(
this.props.children,
this.portalRoot
);
}
}
// Uso:
<Portal portalId="modal-root">
<div className="modal-overlay">
<div className="modal-content">
{this.props.children}
</div>
</div>
</Portal>
Ver más: Render Props para Control de Teclado
A continuación, un ejemplo avanzado de Render Props para manejar atajos de teclado:
class KeyboardHandler extends React.Component {
state = {
pressedKeys: new Set()
};
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('keyup', this.handleKeyUp);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('keyup', this.handleKeyUp);
}
handleKeyDown = (event) => {
this.setState(prev => ({
pressedKeys: new Set([...prev.pressedKeys, event.key])
}));
};
handleKeyUp = (event) => {
this.setState(prev => {
const newKeys = new Set(prev.pressedKeys);
newKeys.delete(event.key);
return { pressedKeys: newKeys };
});
};
isKeyPressed = (key) => {
return this.state.pressedKeys.has(key);
};
render() {
return this.props.children({
pressedKeys: this.state.pressedKeys,
isKeyPressed: this.isKeyPressed
});
}
}
// Uso:
<KeyboardHandler>
{({ isKeyPressed }) => (
<div>
{isKeyPressed('Control') && isKeyPressed('s') && (
<Notification message="Guardando..." />
)}
</div>
)}
</KeyboardHandler>
Mejores Prácticas
- Nombrado descriptivo: Usa nombres claros para las props que pasas al children. Por ejemplo, en lugar de solo
{d => ...}, usa{({ data, loading, error }) => ...}. - Evita funciones en render: No definas funciones flecha dentro del método render del componente padre, ya que esto crea nuevas referencias en cada render.
- Documenta las props: Si creas un componente de Render Props reutilizable, documenta qué datos/métodos recibe la función children.
- Combina con Memo: Si necesitas optimizar el rendimiento, considera usar
React.memoouseMemopara evitar re-renderizados innecesarios.
"Los Render Props son como un contrato: el componente padre promete proporcionar ciertos datos y métodos, mientras que el componente hijo decide exactamente cómo usarlos para renderizarse."
Resumen
El patrón Render Props es una herramienta poderosa para compartir lógica visual entre componentes React. Su principal ventaja es la flexibilidad: mientras que los HOC envuelven componentes, los Render Props te dan control total sobre el renderizado.
Aunque los Hooks han simplificado muchos casos de uso, Render Props sigue siendo relevante especialmente cuando:
- Trabajas con librerías existentes que usan este patrón
- Necesitas compartir lógica de renderizado compleja
- Quieres mantener una jerarquía de componentes más plana
¿Cuál es la principal ventaja del patrón Render Props sobre los Higher-Order Components (HOC)?
- A) Es más rápido en ejecución
- B) Permite mayor flexibilidad en el renderizado y evita el "wrapper hell"
- C) No requiere usar clases
- D) Funciona solo con hooks