Higher-Order Components (HOCs) no React e React Native

React 17 de Jan de 2018

Os High-Order Components são formas de reutilizarmos lógicas repetidas em mais componentes de uma forma prática no React. O conceito é utilizado por várias bibliotecas e vai muito além de compartilhar estilos (que não é seu intuito principal). Você provavelmente já se deparou com Higher-Order Components por aí, eles possuem essa sintaxe:

class Post extends Component {...}

export default withSomeData(Post);

A função withSomeData estaria recebendo o componente Post como parâmetro e retornando um novo componente com novos comportamentos.

Na maioria das vezes são repassadas novas funcionalidades aos componente encapsulados pelo HOC através das props do componente.

Construindo nosso HOC

Chega de conceitos, bora pro código ver como isso funciona! Imaginemos uma situação em que dois componentes dependam de uma mesma informação coletada da API, pode ser uma lista de repositórios do Github, mas eles renderizam esses dados de forma distinta não podendo ser criado apenas um componente que lista esses dados, precisamos apenas passar ao componente toda lista de repositórios coletados do backend. Primeiro, vamos criar nosso HOC:

import React, { Component } from 'react';

export function withRepoData(WrappedComponent, username) {
  return class extends Component {
    state = {
      data: [],
      loading: true,
    }

    async componentDidMount() {
      const response = await fetch(`https://api.github.com/users/${username}/repos`);
      const data = await response.json();

      this.setState({ data, loading: false });
    }

    render() {
      return <WrappedComponent repoData={this.state} {...this.props} />;
    }
  };
}

Vamos notar algumas coisas no código acima:

  1. O primeiro parâmetro recebido quase sempre é o componente a ser encapsulado com a lógica do HOC;
  2. Você pode repassar quantos parâmetros quiser e nesse caso estou pegando o nome de usuário a buscar os repositórios no Github;
  3. O HOC deve retornar um novo componente que por sua vez por ter seu próprio estado e propriedades;
  4. O método render do HOC deve chamar o componente encapsulado repassando todas props com {...this.props} adicionando as propriedades adicionais manualmente.

Agora com nosso HOC construído podemos agregá-lo em outros componentes de nosso código para compartilhar a lógica de busca de repositórios à API do Github sempre que necessário. No exemplo a seguir tenho um componente de lista de repositórios que depende do HOC withRepoData:

import React from 'react';
import { withRepoData } from './hocs/withRepoData';

const RepoList = ({ repoData }) => {
  console.log(repoData); // Just logging

  if (repoData.loading) return <p>Carregando...</p>;

  return repoData.data.map(repo => <p key={repo.id}>{repo.full_name}</p>);
}

export default withRepoData(RepoList, 'diego3g');

Veja que importo a função withRepoData e na hora de criar meu componente RepoList não faço a exportação com export default do mesmo, ao invés disso, exporto o componente encapsulado com nosso HOC no fim do arquivo. Além disso, como repassamos a propriedade repoList no nosso arquivo do Higher-Order Component, podemos acessá-la e renderizar de acordo com suas características.

No exemplo acima faço uma verificação da propriedade loading e mostro a lista apenas quando temos os resultados no estado do HOC resultando no seguinte:

Dessa forma agora podemos utilizar o HOC para agregar a lógica de busca de repositórios à mais componentes da nossa aplicação.

Enhanced-style HOC’s

Outra forma de utilizarmos os HOC’s de uma maneira mais legível é adotarmos o enhanced-style. Esse conceito é utilizado por bibliotecas como Redux, Apollo, etc, e tem como principal diferença do modelo comum o retorno de uma função pelo HOC ao invés do componente, ficando assim:

export function withRepoData(username) {
  return WrappedComponent => {
    return class extends Component {
      // código do HOC
    };
  }
}

Dessa forma, o componente encapsulado é recebido apenas na função de retorno ao invés de um parâmetro no HOC e podemos utilizar uma sintaxe semelhante a seguir:

export default withRepoData('diego3g')(RepoList);

Veja que repasso o nosso componente em uma segunda série de parâmetros, isso porque como o retorno do HOC é uma função, podemos diretamente chamá-la com os parênteses. Mas qual a vantagem disso?

O principal diferencial desse modelo de utilização dos HOC’s está na composição do React. Como provavelmente teremos muitos Higher-Order Componentes no nosso código criados por nós e por bibliotecas auxiliares como Redux, Apollo, Formik, etc etc etc, precisaremos muitas vezes agregar a lógica de múltiplos HOC’s a um único componente e para isso precisamos utilizar a função compose presente na maioria das bibliotecas que utilizam o padrão de HOC, por exemplo, o Redux.

Imaginemos que precisamos adicionar nosso HOC withRepoData e o connect do Redux a um mesmo componente, nosso código ficaria assim:

const RepoList = ({ repoData }) => {...};

export default connect(mapStateToProps, mapDispatchToProps)(withRepoData(RepoList));

Feio né? Um HOC, recebendo outro HOC como parâmetro que chama, através de composição, nosso componente RepoList.

What?!

Utilizando o compose teremos uma sintaxe muito mais clara:

import { compose } from 'redux';

const RepoList = ({ repoData }) => {...};

export default compose(
  withRepoData('diego3g'),
  connect(mapStateToProps, mapDispatchToProps),
)(RepoList);

O compose faz o papel de concatenar HOCs e repassar nosso componente RepoList a cada um deles nos trazendo o componente resultante como retorno no fim.

Utilizando decorators

Utilizando o enhanced-style te dá a possibilidade de trabalhar com decorators. Por enquanto essa feature ainda não está presente nas versões estáveis da ECMAScript, por isso, caso deseje utilizar, basta instalar o plugin https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy.

Com os decorators, utilizamos o @ para repassar HOCs ao componente e nossa sintaxe ficaria ainda menos verbosa:

@connect(mapStateToProps, mapDispatchToProps)
@withRepoData('diego3g')
const RepoList = ({ repoData }) => {...};

Ufa… acabou!

Apesar de parecer complicado muitas vezes, o conceito de HOC é muito simples e está totalmente ligado ao conceito DRY (Don’t Repeat Yourself), ou seja, evite repetições, por isso compartilhar lógica semelhante entre componentes através dos Higher-Order Componentes é uma ótima prática no React.

E aí dev, conhecia esse conceito? Não esquece de deixar seu comentário e palmas por aqui e lembrando que nossa conversa continua pela comunidade da Rocketseat.

Marcadores

Diego Fernandes

Programador full-stack, apaixonado pelas melhores tecnologias de desenvolvimento back-end, front-end e mobile, é co-fundador e CTO na Rocketseat.