Render Props
O termo “render prop” se refere a uma técnica de compartilhar código entre componentes React passando uma prop cujo valor é uma função.
Um componente com uma render prop recebe uma função que retorna um elemento React e a invoca no momento de renderização, não sendo necessário para o componente implementar uma lógica própria.
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
Bibliotecas que usam render props incluem React Router, Downshift e Formik.
Nesse documento vamos discutir por que render props são úteis e como escrevê-las.
Uso de Render Props para Características Transversais
Componentes são as principais unidades de reuso de código em React, mas nem sempre é fácil compartilhar o estado ou comportamento que um componente encapsula para outros componentes utilizarem esses valores.
Por exemplo, o componente abaixo captura a posição do mouse em um aplicativo web:
class MouseTracker extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
<h1>Move the mouse around!</h1>
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
Enquanto o cursor se move pela tela, o componente mostra suas coordenadas (x, y) em um <p>
A questão é: Como podemos reutilizar esse comportamento em outro componente? Em outras palavras, se outro componente precisar saber a posição do cursor, podemos encapsular esse comportamento de forma que seja fácil compartilhá-lo com outros componentes?
Lembrando que componentes são a unidade básica de reuso de código em React, vamos tentar refatorar esse código para usar o componente <Mouse>
, ele encapsula o comportamento que precisamos reutilizar.
// O componente <Mouse> encapsula o comportamento que precisamos...
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/* ...mas como renderizar algo diferente de um <p>? */}
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<>
<h1>Move the mouse around!</h1>
<Mouse />
</>
);
}
}
Agora o componente <Mouse>
encapsula todos os comportamentos associados aos eventos mousemove
e guarda a posição (x, y) do cursor, mas ainda não é completamente reutilizável.
Por exemplo, suponha que temos o componente <Cat>
que renderiza a image de um gato seguindo o mouse na tela.
Poderíamos usar uma prop <Cat mouse={{ x, y }}>
que passaria as coordenadas do mouse para o componente de forma que este saberia onde posicionar a imagem na tela.
Inicialmente, você poderia tentar renderizar <Cat>
dentro do método render do <Mouse>
, assim:
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class MouseWithCat extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/*
Poderíamos simplesmente trocar o <p> por um <Cat> ... mas assim
teríamos que criar um componente <MouseWithSomethingElse>
separado toda vez que precisarmos usá-lo, então <MouseWithCat>
não é muito reutilizável ainda.
*/}
<Cat mouse={this.state} />
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<MouseWithCat />
</div>
);
}
}
Essa abordagem funciona para o nosso caso em específico, mas ainda não atingimos o objetivo de encapsular o comportamento de uma maneira completamente reutilizável. Agora, toda vez que precisarmos da posição do mouse para outro caso, teremos que criar um novo componente (ou seja, outro <MouseWithCat>
) que renderiza algo especificamente para esse caso.
Aqui é onde a render prop se encaixa: Ao invés de escrever um componente <Cat>
dentro de <Mouse>
, e mudar diretamente a saída renderizada, podemos passar uma função como prop para <Mouse>
, que vai chamá-la para determinar o que renderizar dinamicamente- essa é a render prop.
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/*
No lugar de fornecer uma representação estática do que <Mouse> deve
renderizar, use a `render` prop para determinar o que renderizar
dinamicamente.
*/}
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
Agora, no lugar de clonar o componente <Mouse>
e escrever o método render
para resolver um caso específico, nós passamos uma render prop que <Mouse>
pode usar para determinar o que ele renderiza dinamicamente.
Portanto, uma render prop é uma função passada nas props que um componente utiliza para determinar o que renderizar.
Essa técnica torna o comportamento que precisamos compartilhar extremamente portátil. Para acessar esse comportamento, basta renderizar um <Mouse>
com uma render
prop que dirá o que renderizar com o (x, y) atual do cursor.
Algo interessante para notar sobre render props é que você pode implementar a maioria dos higher-order components (HOC) utilizando um componente qualquer com uma render prop. Por exemplo, se você preferir ter um HOC withMouse
no lugar de um componente <Mouse>
, você poderia simplesmente criar um utilizando o <Mouse>
com uma render prop.
// Se você realmente quer usar HOC por alguma razão, você
// pode facilmente criar uma usando um componente qualquer
// com uma render prop!
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}
Então, utilizar uma render prop torna possível qualquer um dos padrões citados.
Usando outras Props além de render
É importante lembrar que mesmo o nome padrão sendo “render props”, não quer dizer que você deve ter uma prop chamada render
para usar esse padrão. Na verdade, qualquer prop que é uma função usada por um componente para determinar o que renderizar é uma “render prop”.
Embora os exemplos acima usem a palavra render
, poderíamos usar a prop children
!
<Mouse children={mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>
Lembre-se, a prop children
não precisar estar nomeada na lista de “atributos” do seu elemento JSX. Ao invés disso, você pode inserí-la dentro do seu elemento!
<Mouse>
{mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}
</Mouse>
Você pode ver essa técnica sendo usada na API react-motion.
Dado que essa é uma técnica um pouco incomum, quando você estiver criando uma API como essa, você provavelmente vai querer definir nos seus propTypes
que children
deve ser uma função.
Mouse.propTypes = {
children: PropTypes.func.isRequired
};
Avisos
Tenha cuidado ao utilizar Render Props num React.PureComponent
Usar uma render prop pode anular a vantagem de utilizar React.PureComponent
se você criar uma função dentro de um método render
. Isso se deve à comparação superficial de prop sempre retornar false
para novas props, e, nesse caso, cada render
vai gerar um novo valor para a render prop.
Por exemplo, continuando com o componente <Mouse>
acima, se Mouse
estendesse React.PureComponent
no lugar de React.Component
, nosso exemplo seria assim:
class Mouse extends React.PureComponent {
// Mesma implementação de antes...
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
{/*
Isso é ruim! O valor da prop `render` vai ser
diferente para cara render.
*/}
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
Nesse exemplo, cada vez que <MouseTracker>
renderiza, ele gera uma nova função como valor da prop <Mouse render>
, anulando assim o efeito de <Mouse>
estender React.PureComponent
!
Para contornar esse problema, você pode definir a prop como um método de instância:
class MouseTracker extends React.Component {
// Definindo como um método de instância, `this.renderTheCat`
// sempre se refere a *mesma* função quando chamamos na
// renderização
renderTheCat(mouse) {
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={this.renderTheCat} />
</div>
);
}
}
Nos casos onde você não pode definir a prop estaticamente (por exemplo, quando você precisa esconder as props e o estado do componente), <Mouse>
deveria estender React.Component
.