DEV Community

Cover image for React Higher-Order Components (HOCs)
Ivan Trindade
Ivan Trindade

Posted on

React Higher-Order Components (HOCs)

Componentes de ordem superior em React, também conhecido como HOCs em inglês, são um padrão avançado em React (ao lado de Render Props Components). Componentes de ordem superior podem ser usados para vários casos de uso. Quero escolher um caso de uso, a renderização condicional com componentes de ordem superior.

1. Primeiro, ele deve ensinar sobre os componentes de ordem superior do React com caso de uso de renderição condicional. Lembre-se de que alterar a aparência de um componente com um componente de ordem superior, especificamente no contexto de renderização condicional, é apenas um dos vários casos de uso de HOCs. Por exemplo, você pode usá-los para ativar o estado local ou para alterar props também.

2. Em segundo lugar, mesmo que você já conheça HOcs, o artigo vai um pouco além ao compor componentes de ordem superior em React e aplicar princípios de programação funcional. Você saberá como usar Higher-Order Components de forma elegante.

Para aprender sobre React Higher Order Components, o artigo enfoca o caso de uso de renderização condicional. Uma renderização condicional no React, pode ser aplicada de várias maneiras. Você pode usar instruções if-else, operador ternário ou operador lógico &&. Você pode ler mais sobre as diferentes formas na documentação oficial.

React Hooks vs Higher-Order Components

Mesmo no React moderno, sou um defensor dos componentes de ordem superior no React. Embora a maioria dos desenvolvedores diga que os React Hooks moveram o React mais na direção de programação funcional, eu digo que é exatamente o oposto. Componentes de ordem superior nos permitem aplicar princípios de programação funcional em componentes, adotando a composição. Os React Hooks, em contraste, transformaram componentes de funções puras (no sentido de programação funcional) em bestas carregadas de estado/efeitos colaterais.

Enfim, ambos têm o direito de existir. Enquanto React Hooks são o status quo para dar sabor a componentes de função com detalhes de implementação (por exemplo, estado, efeitos colaterais) de dentro, React Higher-Order Components dá sabor a função (e componentes de classe) de fora. HOCs são o escudo perfeito para proteger um componente antes que o componente real execute seus detalhes de implementação (por exemplo, React Hooks). Veremos a seguir um caso de uso específico em que isso é verdade.

Higher-Order Components: Caso de uso

Começaremos com um problema onde componentes de ordem superior em React, podem ser usados como solução. Abaixo teremos um componente de lista, que está lá apenas para renderizar uma lista de itens. O componente TodoList recebe seus dados do componente App:

import * as React from 'react';

const TODOS = [
  { id: '1', task: 'Do this', completed: true },
  { id: '2', task: 'Do that', completed: false },
];

const App = () => {
  return <TodoList data={TODOS} />;
};

const TodoList = ({ data }) => {
  return (
    <ul>
      {data.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </ul>
  );
};

const TodoItem = ({ item }) => {
  return (
    <li>
      {item.task} {item.completed.toString()}
    </li>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

No entanto, em uma aplicação do mundo real, esses dados seriam obtidos de uma API externa. A função a seguir mocka essa API de dados, para manter o exemplo leve. No entanto, pense apenas em fetchData() como uma função de blackbox, que retorna dados eventualmente:

const TODOS = [
  { id: '1', task: 'Do this', completed: true },
  { id: '2', task: 'Do that', completed: false },
];

const fetchData = () => {
  return { data: TODOS };
};

const App = () => {
  const { data } = fetchData();

  return <TodoList data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

A aplicação renderiza a lista com seus itens. Mas na maioria das vezes, isso não é suficiente, porque você tem que se preocupar com todos os casos extremos. Quais são esses casos de que estou falando?

Primeiro, o que acontece se seus dados foram null antes de serem buscados de forma assíncrona na API? Você aplicaria uma renderização condicional para desativar sua renderização anteriormente:

const fetchData = () => {
  return { data: null };
};

const App = () => {
  const { data } = fetchData();

  if (!data) return <div>No data loaded yet.</div>;

  return <TodoList data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

Em segundo lugar, o que acontece se seus dados não forem nulos, mas vazios? Você mostraria uma mensagem em uma renderização condicional, para fornecer feedback ao usuário e uma experiência UX aprimorada:

const fetchData = () => {
  return { data: [] };
};

const App = () => {
  const { data } = fetchData();

  if (!data) return <div>No data loaded yet.</div>;
  if (!data.length) return <div>Data is empty.</div>;

  return <TodoList data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

Em terceiro lugar, como os dados chegam de forma assíncrona de seu back-end, você deseja mostrar um indicador de carregamento caso os dados estejam pendentes em uma solicitação. Portanto, você obteria mais uma propriedade, como isLoading, para saber sobre o estado de carregamento:

const fetchData = () => {
  return { data: null, isLoading: true };
};

const App = () => {
  const { data, isLoading } = fetchData();

  if (isLoading) return <div>Loading data.</div>;
  if (!data) return <div>No data loaded yet.</div>;
  if (!data.length) return <div>Data is empty.</div>;

  return <TodoList data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

Ok, não quero tornar este exemplo mais complexo (por exemplo, adicionar outro estado de erro), mas você entendeu que muitos casos extremos podem ser somados em um único componente para apenas este caso de uso.

Embora isso seja apenas adicional verticalmente para um componente cobrir cada caso de borda, imagine a renderização condicional semelhante para outros componentes que executam essa busca de dados. Inserindo componentes de ordem superior, porque eles podem ser usados para proteger esses casos extremos como recursos reutilizáveis.

React's Higher Order Components

Os Higher Order Components (HOCs), derivam do conceito de Higher-Order Functions (HOF) que é assim chamado sempre que recebe uma função como argumento ou retorna uma função em sua declaração de retorno. O último é ilustrado no próximo exemplo, como uma versão abreviada usando uma arrow function em Javascript:

const multiply = (multiplier) => (multiplicand) =>
  multiplicand * multiplier;

const product = multiply(3)(4);

console.log(product);
// 12
Enter fullscreen mode Exit fullscreen mode

Embora seja totalmente bom apenas pegando os dois argumentos em apernas uma função:

const multiply = (multiplier, multiplicand) =>
  multiplicand * multiplier;

const product = multiply(3, 4);

console.log(product);
// 12
Enter fullscreen mode Exit fullscreen mode

Pode-se ver como o uso de HOFs com composição de funções pode levar á programação funcional em Javascript:

const multiply = (multiplier) => (multiplicand) =>
  multiplicand * multiplier;

const subtract = (minuend) => (subtrahend) =>
  subtrahend - minuend;

const result = compose(
  subtraction(2),
  multiply(4),
)(3);

console.log(result);
// 10
Enter fullscreen mode Exit fullscreen mode

Sem entrar em mais detalhes sobre HOFs em JavaScript aqui, vamos percorrer todo esse conceito ao falar sobre HOCs em React. Lá iremos percorrer funções normais, funções que recebem outras funções (componentes de função) como argumentos e funções que são compostas umas nas outras, como você viu no último trecho de código.

Os HOCs pegam qualquer componente React como componente de entrada e retornam uma versão aprimorada dele como componente de saída. Em nosso exemplo, o objetivo seria proteger especificamente todos os casos extremos de renderização condicional entre componente pai (App) e o componente filho (TodoList), porque nenhum deles quer ser incomodado por eles.

Component => EnhancedComponent
Enter fullscreen mode Exit fullscreen mode

Um projeto para um componente de ordem superior que está apenas pegando um componente como entrada e retornando o mesmo componente como saída tem sempre a seguinte aparência no código real:

const withHigherOrderComponent = (Component) => (props) =>
  <Component {...props} />;
Enter fullscreen mode Exit fullscreen mode

Ao criar um componente de ordem superior, você sempre começará com esta versão dele. Um componente de ordem superior sempre vem com o prefixo with (o mesmo que um React Hook sempre vem com o prefixo use). Agora você pode chamar esse projeto de um HOC em qualquer componente sem alterar nada relacionado a aplicação:

const withHigherOrderComponent = (Component) => (props) =>
  <Component {...props} />;

const App = () => {
  const { data, isLoading } = fetchData();

  if (isLoading) return <div>Loading data.</div>;
  if (!data) return <div>No data loaded yet.</div>;
  if (!data.length) return <div>Data is empty.</div>;

  return <TodoList data={data} />;
};

const BaseTodoList = ({ data }) => {
  return (
    <ul>
      {data.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </ul>
  );
};

const TodoList = withHigherOrderComponent(BaseTodoList);
Enter fullscreen mode Exit fullscreen mode

Entender o último trecho de código é a parte mais importante desse tutorial. O componente de ordem superior que criamos (aqui: withHigherOrderComponent) recebe um componente como argumento. Em nosso caso, renomeamos o TodoList para BaseTodoList como componente de entrada e retornamos um novo TodoList como componente aprimorado a partir dele. O que recebemos de volta é essencialmente um componente de função agrupado:

// o que recebemos de volta quando estamos chamando o HOC
(props) =>
  <Component {...props} />;
Enter fullscreen mode Exit fullscreen mode

Basicamente, é apenas outro componente de função que passa por todas as props do React sem tocá-las. Em sua essência, nada acontece aqui, o componente original apenas é envolvido em outro componente de função (arrow function), que não adiciona mais lógica a ele.

Portanto, o componente retornado não é aprimorado. Mas isso está prestes a mudar. Vamos tornar este componente de ordem superior útil adicionando todas as renderizações condicionais como aprimoramento:

const withConditionalFeedback = (Component) => (props) => {
  if (props.isLoading) return <div>Loading data.</div>;
  if (!props.data) return <div>No data loaded yet.</div>;
  if (!props.data.length) return <div>Data is empty.</div>;

  return <Component {...props} />;
};

const App = () => {
  const { data, isLoading } = fetchData();

  return <TodoList data={data} isLoading={isLoading} />;
};

const BaseTodoList = ({ data }) => {
  return (
    <ul>
      {data.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </ul>
  );
};

const TodoList = withConditionalFeedback(BaseTodoList);
Enter fullscreen mode Exit fullscreen mode

A última refatoração acima, moveu toda a lógica de implementação da renderização condicional do componente App para o componente de ordem superior. É o lugar perfeito, pois desta forma o componente App nem seu componente filho se incomodam com esse detalhe.

Você pode imaginar como isso pode não ser o ajuste perfeito para React Hooks. Primeiro, geralmente um hook não retorna JSX condicional. E em segundo lugar, um hook não está protegendo um componente do lado de fora, mas adiciona detalhes de implementação por dentro.

Isso é tudo o que você precisa saber sobre os fundamentos dos HOCs. Você pode começar a usá-los ou ir ainda mais longe adicionando configuração ou composição aos seus componentes de ordem superior.

Configuração de componentes de ordem superior

Se um Componente de Ordem Superior toma apenas um Componente e nada mais como argumento, tudo o que está relacionado aos detalhes de implementação é decidido pelo próprio HOC. No entanto, como temos funções JavaScript, podemos passar mais informações como argumentos de fora, para obter mais controle como usuário desse componente de ordem superior.

const withHigherOrderComponent = (Component, configuration) =>
  (props) => <Component {...props} />;
Enter fullscreen mode Exit fullscreen mode

Somente componentes de ordem superior precisam desse tipo de configuração extra adicionada de forma externa. Mantendo-o mais amigável para o paradigma de programação funcional (veja a composição de HOCs mais tarde), optamos pela configuração por meio de uma função separada preventivamente:

const withHigherOrderComponent = (configuration) => (Component) =>
  (props) => <Component {...props} />;
Enter fullscreen mode Exit fullscreen mode

Dessa forma, configurar um componente de ordem superior é essencialmente apenas a adição de outra função de encapsulamento em torno dele. Mas por que se preocupar com isso em primeiro lugar? Vamos voltar ao nosso caso de uso anterior de renderização de feedback condicional para nossos usuários. No momento, o feedback é bastante genérico (por exemplo, "Data is empty."). Ao configurar o HOC de fora, podemos decidir qual feedback mostrar aos nossos usuários:

const withConditionalFeedback = (dataEmptyFeedback) => (Component)
  => (props) => {
    if (props.isLoading) return <div>Loading data.</div>;
    if (!props.data) return <div>No data loaded yet.</div>;

    if (!props.data.length)
      return <div>{dataEmptyFeedback || 'Data is empty.'}</div>;

    return <Component {...props} />;
  };

...

const TodoList = withConditionalFeedback('Todos are empty.')(
  BaseTodoList
);
Enter fullscreen mode Exit fullscreen mode

Veja como ainda estamos usando um fallback genérico, caso dataEmptyFeedback não seja fornecido de fora. Vamos continuar exibindo as outras mensagens de feedback opcionais também:

const withConditionalFeedback =
  ({ loadingFeedback, noDataFeedback, dataEmptyFeedback }) =>
  (Component) =>
  (props) => {
    if (props.isLoading)
      return <div>{loadingFeedback || 'Loading data.'}</div>;

    if (!props.data)
      return <div>{noDataFeedback || 'No data loaded yet.'}</div>;

    if (!props.data.length)
      return <div>{dataEmptyFeedback || 'Data is empty.'}</div>;

    return <Component {...props} />;
  };

...

const TodoList = withConditionalFeedback({
  loadingFeedback: 'Loading Todos.',
  noDataFeedback: 'No Todos loaded yet.',
  dataEmptyFeedback: 'Todos are empty.',
})(BaseTodoList);
Enter fullscreen mode Exit fullscreen mode

Para manter todos eles opt-in, estamos passando um objeto de configuração em vez de vários argumentos. Dessa forma, não precisamos passar null como argumento se quisermos aceitar o segundo argumento, mas não o primeiro.

Afinal, sempre que você quiser configurar um Higher-Order Component de fora, envolva o HOC em outra função e forneça um argumento como objeto de configuração para ele. Então você tem que chamar o HOC de fora duas vezes. A primeira vez para configurá-lo e a segunda vez, para aprimorar o componente real com os detalhes de implementação.

Composição de componentes de ordem superior

O que é ótimo sobre os componentes de ordem superior é que eles são apenas funções que permitem dividir a funcionalidade em várias funções. Pegue nosso componente de ordem superior anterior (sem configuração ainda) como exemplo, dividindo-o em vários componentes de ordem superior:

const withLoadingFeedback = ( Componente ) => ( props ) => {       
  if ( props . isLoading ) return < div > Carregando dados. </ div > ;   
  return < Componente { ... props } /> ;   
} ;

const withNoDataFeedback = ( Component ) => ( props ) => {       
  if ( ! props . data ) return < div > Nenhum dado carregado ainda. </ div > ;   
  return < Componente { ... props } /> ;   
} ;

const withDataEmptyFeedback = ( Componente ) => ( props ) => {       
  if ( ! props . data . length ) return < div > Os dados estão vazios. </ div > ;   
  return < Componente { ... props } /> ;   
} ;
Enter fullscreen mode Exit fullscreen mode

Em seguida, você pode aplicar cada componente de ordem superior individualmente:

const TodoList = withLoadingFeedback (   
  withNoDataFeedback (
    withDataEmptyFeedback ( BaseTodoList )
  )
) ;
Enter fullscreen mode Exit fullscreen mode

Existem duas advertências importantes ao aplicar vários HOCs em um componente:

  • Em primeiro lugar, a ordem importa. Se a prioridade de um (por exemplo withLoadingFeedback) for maior que a do outro (por exemplo withNoDataFeedback), ele deve ser o HOC externo mais chamado, porque você deseja renderizar o indicador de carregamento (se isLoading for true) em vez de "No data loaded yet."

  • E segundo, os HOCs podem depender uns dos outros (o que os torna frequentemente uma armadilha). Por exemplo, o withDataEmptyFeedback depende do seu irmão withNoDataFeedback para a verificação nula de !data.

Se o último não estivesse lá, haveria uma exceção de ponteiro nulo para a verificação vazia de !props.data.length. No entanto, o HOC withLoadingFeedback é independente.

De qualquer forma, chamar a função dentro da função parece prolixo. Como temos funções, podemos fazer uso dos princípios de programação funcional aqui, compondo as funções umas nas outras de uma maneira mais legível:

const compose = (...fns) =>
  fns.reduceRight((prevFn, nextFn) =>
    (...args) => nextFn(prevFn(...args)),
    value => value
  );

const TodoList = compose(
  withLoadingFeedback,
  withNoDataFeedback,
  withDataEmptyFeedback
)(BaseTodoList);
Enter fullscreen mode Exit fullscreen mode

Essencialmente, a função compose() pega todos os argumentos passados ​​(devem ser funções) como um array de funções e os aplica da direita para a esquerda no argumento da função retornada. Vale a pena notar que a função compose() também vem como função com muitas bibliotecas de utilitários (por exemplo, Lodash). No entanto, a implementação mostrada é suficiente para este caso de uso.

Por último, mas não menos importante, queremos trazer de volta a configuração anterior de nossos componentes de ordem superior. Primeiro, adapte os componentes atômicos de ordem superior para usar uma configuração novamente, mas desta vez apenas uma string em vez de um objeto, porque queremos apenas configurá-lo com uma mensagem de feedback (que não é opcional desta vez):

const withLoadingFeedback = (feedback) => (Component) => (props) => {
  if (props.isLoading) return <div>{feedback}</div>;
  return <Component {...props} />;
};

const withNoDataFeedback = (feedback) => (Component) => (props) => {
  if (!props.data) return <div>{feedback}</div>;
  return <Component {...props} />;
};

const withDataEmptyFeedback = (feedback) => (Component) => (props) => {
  if (!props.data.length) return <div>{feedback}</div>;
  return <Component {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

Em segundo lugar, forneça esta configuração opcional ao chamar as funções de ordem superior:

const TodoList = compose(
  withLoadingFeedback('Loading Todos.'),
  withNoDataFeedback('No Todos loaded yet.'),
  withDataEmptyFeedback('Todos are empty.')
)(BaseTodoList);
Enter fullscreen mode Exit fullscreen mode

Você pode ver como a composição de funções, além de usar uma função de empacotamento extra para a configuração, permite que nós, como desenvolvedores, sigamos os princípios de programação funcional aqui. Se um dos componentes de ordem superior não aceitasse configuração, ele ainda poderia ser usado nesta composição (apenas não o chamando como os outros que aceitam configuração).


Conclusão

Espero que este tutorial tenha ajudado você a aprender o conceito avançado de componentes de ordem superior no React, ao mesmo tempo em que deixa claro quando usá-lo em React Hooks. Vimos o caso de uso para HOCs no contexto de renderização condicional, no entanto, existem muitos mais (por exemplo, props/alteração de estado, connect de react-redux que conecta um componente ao armazenamento global).

Por último, mas não menos importante, espero que o guia tenha inspirado você sobre como aplicar paradigmas de programação funcional em React com componentes de ordem superior usando funções de ordem superior para configurações opcionais, mantendo funções puras e compondo funções em cada um.

Top comments (0)