C

Padrões React: Render Props

14 de set. de 2023

Fala, galera! Esse artigo faz parte de uma série focada em padrões do React. Recomendo dar uma conferida nos últimos posts:

Hoje iremos falar sobre o padrão render props, mais um padrão que faz uso das características das funções de primeira classe do javascript.

Segundo a documentação do react:

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.

O que isso quer dizer?

De forma bem simplificada, uma render prop é uma função que irá ser responsável por renderizar um componente qualquer. Dessa forma é possível compartilhar estados e funções entre componentes de maneira facilitada.

Essa é um exemplo de uma render prop:

<Container render={name => <h1>Hello {name}</h1>} />

Para fazer mais sentido, vamos aplicar render props em um exemplo mais detalhado.

O problema

Você possui um componente que é responsável por capturar as coordenadas de latitude e longitude do usuário. O código abaixo faz exatamente isso.

function Geolocation() {
  const [coords, setCoords] = useState({ lat: null, long: null });

  function getCoords() {
    if (window.navigator && window.navigator.geolocation) {
      window.navigator.geolocation.getCurrentPosition(position => {
        setCoords({
          lat: position.coords.latitude,
          long: position.coords.longitude,
        });
      });
    }
  }

  return (
    <div>
      <h1>Coordenadas</h1>
      <button onClick={getCoords}>Pegar coordenadas</button>
      <p>
        {coords.lat} {coords.long}
      </p>
    </div>
  );
}

O resultado do código acima é o seguinte:

Print do resultado do component que captura a localização do usuário

Imagine o cenário em que você precisa construir dois cards, um que pega a coordenada do usuário assim que a tela carrega e outro que pega a coordenada apenas quando o usuário clica no botão. Como você resolveria isso?

Uma forma bem simples de resolver esse problema é replicar a lógica nos dois cards.

function CardWithGeolocationWhenPageLoad() {
  const [coords, setCoords] = useState({ lat: null, long: null });

  useEffect(() => {
    getCoords();
  }, []);
  function getCoords() {
    if (window.navigator && window.navigator.geolocation) {
      window.navigator.geolocation.getCurrentPosition(position => {
        setCoords({
          lat: position.coords.latitude,
          long: position.coords.longitude,
        });
      });
    }
  }

  return (
    <div>
      <h1>Coordenadas quando a página carregar</h1>
      <p>
        {coords.lat} {coords.long}
      </p>
    </div>
  );
}
function CardWithGeolocationWhenButtonIsClicked() {
  const [coords, setCoords] = useState({ lat: null, long: null });

  function getCoords() {
    if (window.navigator && window.navigator.geolocation) {
      window.navigator.geolocation.getCurrentPosition(position => {
        setCoords({
          lat: position.coords.latitude,
          long: position.coords.longitude,
        });
      });
    }
  }

  return (
    <div>
      <h1>Coordenadas quando o botão for clicado</h1>
      <button onClick={getCoords}>Pegar coordenadas</button>
      <p>
        {coords.lat} {coords.long}
      </p>
    </div>
  );
}

Problema resolvido, certo? Imagine que agora nossos cards precisam fazer uma chamada para uma api informando quais coordenadas foram capturadas e qual o horário da captura.

Ai você pensa: "Só ir nos dois cards e adicionar essa lógica". Mas e se mais uma lógica surgir? E se surgir um terceiro card que também precisa dessa lógica? A verdade é que vai chegar um momento que será insustentável manter todo esse código. E é ai que o render props brilha! Pois com ele vamos conseguir concentrar toda essa lógica de coordenadas em um único lugar e compartilhar com N componentes.

Aplicando render props

Com o render props nossa implementação do component Geolocation ficaria assim:

function Geolocation(props) {
  const [coords, setCoords] = useState({ lat: null, long: null });

  function getCoords() {
    if (window.navigator && window.navigator.geolocation) {
      window.navigator.geolocation.getCurrentPosition(position => {
        setCoords({
          lat: position.coords.latitude,
          long: position.coords.longitude,
        });
      });
    }
  }

  return <div>{props.render({ coords, getCoords })}</div>;
}

Ao invés de renderizar o botão e as coordenadas, agora nossa função recebe uma prop chamada render. Essa prop é uma função que irá receber um objeto como parâmetro(poderia ser qualquer dado) e nesse objeto iremos enviar o estado coords e a função getCoords que poderão ser utilizados por qualquer componente que for passado na prop render.

A aplicação do Geolocation para construir o CardWithGeolocationWhenPageLoad ficaria assim:

function CardWithGeolocationWhenPageLoad(props) {
  useEffect(() => {
    props.getCoords();
  }, []);

  return (
    <div>
      <h1>Coordenadas quando a página carregar</h1>
      <p>
        {props.coords.lat} {props.coords.long}
      </p>
    </div>
  );
}

<Geolocation
  render={({ coords, getCoords }) => {
    return (
      <CardWithGeolocationWhenPageLoad coords={coords} getCoords={getCoords} />
    );
  }}
/>;

E a implementação e uso do CardWithGeolocationWhenButtonIsClicked também fica extremamente simples.

function CardWithGeolocationWhenButtonIsClicked(props) {
  return (
    <div>
      <h1>Coordenadas quando o botão for clicado</h1>
      <button onClick={props.getCoords}>Pegar coordenadas</button>
      <p>
        {props.coords.lat} {props.coords.long}
      </p>
    </div>
  );
}

<Geolocation
  render={({ coords, getCoords }) => {
    return (
      <CardWithGeolocationWhenButtonIsClicked
        coords={coords}
        getCoords={getCoords}
      />
    );
  }}
/>;

Notou que agora temos toda a lógica referente a coordenadas em um único local? Possibilitando que diversos componentes façam uso dessa lógica? Agora se a gente precisar fazer qualquer modificação no método getCoords só precisamos mexer em um único ponto.

Importante! Aqui no nosso exemplo nós utilizamos uma prop chamada render, porém essa prop pode ser chamada de qualquer coisa que o resultado seria o mesmo. Basta que a prop seja uma função que receba argumentos e retorne um componente.

Também podemos utilizar o render props com os childrens do react. Dessa forma a implementação pode ser feita de uma maneira um pouquinho diferente:

function Geolocation(props) {
  const [coords, setCoords] = useState({ lat: null, long: null });

  function getCoords() {
    if (window.navigator && window.navigator.geolocation) {
      window.navigator.geolocation.getCurrentPosition(position => {
        setCoords({
          lat: position.coords.latitude,
          long: position.coords.longitude,
        });
      });
    }
  }

  return <div>{props.children({ coords, getCoords })}</div>;
}

<Geolocation>
  {({ coords, getCoords }) => {
    return (
      <CardWithGeolocationWhenButtonIsClicked
        coords={coords}
        getCoords={getCoords}
      />
    );
  }}
</Geolocation>;

A ideia é a mesma, pois o children também é uma prop do react. Esse estilo de abordagem é muito comum em libs de animações como o react-motion.

Para entender mais sobre o funcionamento você pode ver e editar o código que acabamos de construir:

Devo utilizar Render Props?

Assim como os HOCs, Render Props são uma ótima solução para manter uma lógica centralizada com a possibilidade aplicá-la para N componentes.

Atualmente existem formas melhores de resolver o mesmo problema que um Render Props se propões utilizando o padrão de hooks, mas esse é um papo para um próximo post.

É importante entender que apesar de existirem formas mais mordernas de compartilhar lógica entre componentes, ainda é muito comum encontrar Render Props em aplicações modernas, um exemplo clássico são algumas libs de animações como o react-motion..

Isso é tudo, pessoal!

Fico feliz que você chegou até aqui e espero que tenha aprendido algo novo ao longo dessa leitura. Em breve irei trazer mais artigos abordando outros padrões do react. Até mais!

Referências