Entendiendo Render props en React

En este artículo entenderemos qué son y cómo funcionan las render props. En el próximo artículo construiremos un popover utilizando este patrón.

Render props es un patrón utilizado en React que consiste en delegar lo que un componente va a renderear a otro componente, la mayoría de la veces, a un padre en el árbol de componentes.

En general usamos render props cuando queremos dar la mayor libertad posible al consumidor de nuestro componente de definir qué es lo que va a renderear, sin atarlo necesariamente a un template específico. Existen varios componentes y librerías populares que hacen uso de este patrón, tales como Formik y Downshift, por nombrar algunos.

Un ejemplo (quizás poco práctico, pero ilustrativo) podría ser un componente que renderea una lista de definiciones a partir de un diccionario (un objeto común y corriente):

import React from 'react';const List = ({ data }) => {
return (
<ul>
{Object.entries(data).map(([ key, description ]) =>
<li key={key}>
<strong>{key}:</strong> {description}
</li>
)}
</ul>
);
};
export default List;

Y ocupamos nuestro componente de la siguiente forma:

import React, { useState } from 'react';
import List from './List';
const App = () => {
const [ definitions ] = useState({
"name": "Frank",
"last-name": "Zappa",
});

return (
<List data={definitions} />
);
};
/* El html generado sería algo así:<ul>
<li>
<strong>name:</strong> Frank</li>
<li>
<strong>last-name:</strong> Zappa</li>
</ul>
*/

El problema con nuestro componente List es que nos hemos atado definitivamente a renderear la llave de cada entrada en nuestro objeto dentro de un tag strong y el resto fuera.

Usando render props podemos invertir la responsabilidad de render a nuestro componente App:

const List = ({ data, render }) => {
return (
<ul>
{Object.entries(data).map(([ key, description ]) =>
<li key={key}>
{render({ key, description })}
</li>
)}
</ul>
);
};
// Y luegoconst App = () => {
const [ definitions ] = setState({
"name": "Frank",
"last-name": "Zappa",
});

return (
<List
data={definitions}
render={({ key, description }) => (
<Fragment>
{key}: <strong>{description}</strong>
</Fragment>
)}

/>
);
};
/* El html generado sería algo así: <ul>
<li>name:
<strong>Frank</strong></li>
<li>last-name:
<strong>Zappa</strong></li>
</ul>
*/

En este caso le hemos pasado una función como prop render al componente List definiendo explícitamente qué es lo que vamos a renderear dentro de cada li.

¿Pero cómo funciona esta magia?

Para comprender cómo funcionan los render props, tenemos que entender que JSX no es más que azúcar sintáctica para React.createElement:

const Hello = ({ name = 'World' }) => {
return <h1>Hello {name}</h1>
}
// Es equivalente a:const Hello = ({ name = 'World' }) => {
return React.createElement("h1", null, "Hello ", name);
}

Y como React.createElement no es más que una función común y silvestre, podemos tratarla como cualquier valor primitivo en JavaScript. Notemos que lo que retorna nuestro componente Hello es la evaluación de una función, es decir, una expresión. Podríamos wrappear la invocación de React.createElement en una IIFE (Immediately Invoked Function Expression) de la siguiente manera:

const Hello = ({ name = 'World' }) => {
return (
(_name) => React.createElement("h1", null, "Hello ", _name)
)(name);
}

Es importante notar que nuestro componente sigue retornando la invocación de una función o en otras palabras, una expresión.

Podemos reescribir la función que he marcado en bold como una función nombrada render:

function render(_name) {
return React.createElement("h1", null, "Hello ", _name);
};

Y reemplazando la función anónima en el componente Hello por nuestra función render obtenemos:

const Hello = ({ name = 'World' }) => {
return (render)(name);
}

Si convertimos nuestro ejemplo de vuelta a JSX terminamos con lo siguiente:

function render(name) {
return <h1>Hello {name}</h1>;
};
const Hello = ({ name = 'World' }) => render(name);

Pues bien, es claro que nuestro componente Hello renderea un pedazo de JSX proveniente de una fuente externa, en este caso de la función render definida en el mismo scope. Podemos explotar este patrón y hacerlo reutilizable si en vez de acceder a la función render a través de un closure, lo hacemos directamente vía props:

const Hello = ({ name = 'World', render }) => render(name);

Y luego ocupamos nuestro componente Hello de la siguiente forma:

const App = () => {
return (
<Hello
render={(name) => <h1>Hello {name}</h1>}
name="Bob"
/>
);
};

Y voilá, tenemos render props.

Por último cabe mencionar que podemos usar cualquier prop en un componente como render prop (salvo algunas reservadas como key y ref), incluso children, por lo que tras un par de ajustes nuestro componente List se podría usar de la siguiente forma:

const List = ({ data, children }) => {
return (
<ul>
{Object.entries(data).map(([ key, description ]) =>
<li key={key}>
{children({ key, description })}
</li>
)}
</ul>
);
};
// Y luegoconst App = () => {
const [ definitions ] = setState({
"name": "Frank",
"last-name": "Zappa",
});

return (
<List data={definitions}>
{({ key, description }) => (
<Fragment>
{key}: <strong>{description}</strong>
</Fragment>
)}
</List>
);
};

Desafortunadamente eso será todo por hoy, en el próximo artículo nos enfocaremos en el desarrollo del popover. Si te gustó la publicación, compártela y no olvides dejar algunos claps 👏.

--

--

Front End Architect at Cornershop.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Osman Cea

Osman Cea

Front End Architect at Cornershop.