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: '100%' }} 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: '100%' }} 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 (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse />
      </div>
    );
  }
}

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: '100%' }} 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: '100%' }} 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.