Componentes de ordem superior em React

Recentemente, publicamos material sobre funções de ordem superior em JavaScript, destinado a quem aprende JavaScript. O artigo que estamos traduzindo hoje é destinado a desenvolvedores iniciantes do React. Ele se concentra nos Componentes de Ordem Superior (HOC).



Princípio DRY e componentes de ordem superior no React


Você não poderá avançar o suficiente no estudo da programação e não se deparar com o quase culto princípio de DRY (não se repita, não repita). Às vezes, seus seguidores vão longe demais, mas, na maioria dos casos, vale a pena lutar pelo cumprimento. Aqui, falaremos sobre o padrão de desenvolvimento React mais popular, que garante a conformidade com o princípio DRY. Trata-se de componentes de ordem superior. Para entender o valor dos componentes de ordem superior, vamos primeiro formular e entender o problema que eles pretendem resolver.

Suponha que você precise recriar um painel de controle semelhante ao painel Listra. Muitos projetos têm a propriedade de desenvolver de acordo com o esquema, quando tudo vai bem até o momento em que o projeto é concluído. Quando você acha que o trabalho está quase terminado, percebe que o painel de controle tem muitas dicas de ferramentas diferentes que devem aparecer quando você passa o mouse sobre determinados elementos.


Painel de controle e dicas de ferramentas

Para implementar essa funcionalidade, você pode usar várias abordagens. Você decidiu fazer isso: determine se o ponteiro está acima de um componente individual e, em seguida, decida se deseja mostrar uma dica para ele ou não. Existem três componentes que precisam ser equipados com funcionalidades semelhantes. Estas são as Info , TrendChart e DailyChart .

Vamos começar com o componente Info . No momento, é um simples ícone SVG.

 class Info extends React.Component { render() {   return (     <svg       className="Icon-svg Icon--hoverable-svg"       height={this.props.height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   ) } } 

Agora precisamos tornar esse componente capaz de determinar se o ponteiro do mouse está acima dele ou não. Você pode usar os eventos do mouse onMouseOut e onMouseOut para isso. A função passada para onMouseOver será chamada se o ponteiro do mouse tiver caído na área do componente e a função passada para onMouseOut será chamada quando o ponteiro sair do componente. Para organizar tudo isso da maneira que é aceita no React, adicionamos a propriedade hovering ao componente, que é armazenada no estado, o que nos permite renderizar novamente o componente, mostrando ou ocultando a dica de ferramenta, se essa propriedade for alterada.

 class Info extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id} />         : null}       <svg         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}         className="Icon-svg Icon--hoverable-svg"         height={this.props.height}         viewBox="0 0 16 16" width="16">           <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />       </svg>     </>   ) } } 

Acabou muito bem. Agora precisamos adicionar a mesma funcionalidade a mais dois componentes - TrendChart e DailyChart . O mecanismo acima para o componente Info funciona bem, o que não está quebrado não precisa ser reparado, então vamos recriar o mesmo em outros componentes usando o mesmo código. Recicle o código para o componente TrendChart .

 class TrendChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='trend'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } } 

Você provavelmente já entendeu o que fazer a seguir. O mesmo pode ser feito com nosso último componente - DailyChart .

 class DailyChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='daily'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } } 

Agora está tudo pronto. Você já deve ter escrito algo semelhante no React. Obviamente, esse não é o pior código do mundo, mas não segue particularmente particularmente o princípio DRY. Como você pode ver, analisando o código do componente, em cada um deles repetimos a mesma lógica.

O problema que nos confronta agora deve se tornar extremamente claro. Este é um código duplicado. Para resolvê-lo, queremos nos livrar da necessidade de copiar o mesmo código nos casos em que o que já implementamos é necessário para um novo componente. Como resolver isso? Antes de falarmos sobre isso, focaremos em vários conceitos de programação que facilitarão bastante o entendimento da solução proposta aqui. Estamos falando de retornos de chamada e funções de ordem superior.

Funções de ordem superior


Funções em JavaScript são objetos de primeira classe. Isso significa que eles, como objetos, matrizes ou cadeias, podem ser atribuídos a variáveis, passados ​​para funções como argumentos ou retornados de outras funções.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } addFive(10, add) // 15 

Se você não está acostumado a esse comportamento, o código acima pode parecer estranho para você. Vamos falar sobre o que está acontecendo aqui. Nomeadamente, passamos a função add para a função addFive como argumento, renomeie-a para addReference e, em seguida, chame-a.

Quando essas construções são usadas, uma função passada como argumento para outra é chamada de retorno de chamada (função de retorno de chamada) e uma função que recebe outra função como argumento é chamada de função de ordem superior.

Nomear entidades na programação é importante, portanto, aqui está o mesmo código usado no qual os nomes são alterados de acordo com os conceitos que eles representam.

 function add (x,y) { return x + y } function higherOrderFunction (x, callback) { return callback(x, 5) } higherOrderFunction(10, add) 

Esse padrão deve lhe parecer familiar. O fato é que, se você usou, por exemplo, métodos de matriz JavaScript, trabalhou com jQuery ou lodash, já usou funções e retornos de chamada de ordem superior.

 [1,2,3].map((i) => i + 5) _.filter([1,2,3,4], (n) => n % 2 === 0 ); $('#btn').on('click', () => console.log('Callbacks are everywhere') ) 

Vamos voltar ao nosso exemplo. E se, em vez de apenas criar a função addFive , desejamos criar a função addTwenty , addTwenty e outras coisas assim. Dado como a função addFive é addFive , teremos que copiar seu código e alterá-lo para criar as funções mencionadas acima com base nele.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } function addTen (x, addReference) { return addReference(x, 10) } function addTwenty (x, addReference) { return addReference(x, 20) } addFive(10, add) // 15 addTen(10, add) // 20 addTwenty(10, add) // 30 

Deve-se notar que nosso código não era tão pesadelo, mas é claro que muitos fragmentos nele são repetidos. Nosso objetivo é que possamos criar tantas funções que adicionem certos números aos números passados ​​a eles ( addFive , addTen , addTwenty e assim por diante) o quanto for necessário, minimizando a duplicação de código. Talvez para conseguir isso, precisamos criar uma função makeAdder ? Esta função pode levar um certo número e um link para a função de add . Como o objetivo dessa função é criar uma nova função que adicione o número passado a ela, podemos fazer com que a função makeAdder retorne uma nova função que contenha um determinado número (como o número 5 no makeFive ) e que possa levar números para adicionar a esse número.

Veja um exemplo da implementação dos mecanismos acima.

 function add (x, y) { return x + y } function makeAdder (x, addReference) { return function (y) {   return addReference(x, y) } } const addFive = makeAdder(5, add) const addTen = makeAdder(10, add) const addTwenty = makeAdder(20, add) addFive(10) // 15 addTen(10) // 20 addTwenty(10) // 30 

Agora podemos criar quantas funções add necessárias, minimizando a quantidade de duplicação de código.

Se for interessante, o conceito de que existe uma determinada função que processa outras funções para que elas possam ser usadas com menos parâmetros do que antes é chamado de "aplicação parcial da função". Essa abordagem é usada na programação funcional. Um exemplo de seu uso é o método .bind usado em JavaScript.

Tudo isso é bom, mas o que o React tem a ver com o problema acima de duplicar o código para processar eventos do mouse ao criar novos componentes que precisam desse recurso? O fato é que, assim como a função de ordem superior makeAdder nos ajuda a minimizar a duplicação de código, o que é chamado de "componente de ordem superior" nos ajuda a lidar com o mesmo problema em um aplicativo React. No entanto, aqui tudo parecerá um pouco diferente. Ou seja, em vez de um esquema de trabalho, durante o qual uma função de ordem superior retorna uma nova função que chama um retorno de chamada, um componente de ordem superior pode implementar seu próprio esquema. Ou seja, ele é capaz de retornar um novo componente que renderiza um componente que desempenha o papel de um "retorno de chamada". Talvez já tenhamos dito muitas coisas, então é hora de passar para exemplos.

Nossa função de ordem mais alta


Esse recurso possui os seguintes recursos:

  • Ela é uma função.
  • Ela aceita, como argumento, um retorno de chamada.
  • Retorna uma nova função.
  • A função que ele retorna pode chamar o retorno de chamada original que foi passado para nossa função de ordem superior.

 function higherOrderFunction (callback) { return function () {   return callback() } } 

Nosso componente de ordem mais alta


Este componente pode ser caracterizado da seguinte maneira:

  • É um componente.
  • Como argumento, é preciso outro componente.
  • Retorna um novo componente.
  • O componente retornado pode renderizar o componente original passado para o componente de ordem superior.

 function higherOrderComponent (Component) { return class extends React.Component {   render() {     return <Component />   } } } 

Implementação de HOC


Agora que nós, em termos gerais, descobrimos exatamente quais ações o componente de ordem superior executa, começaremos a fazer alterações em nosso código React. Se você se lembra, a essência do problema que estamos resolvendo é que o código que implementa a lógica do processamento de eventos do mouse deve ser copiado para todos os componentes que precisam desse recurso.

 state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) 

Diante disso, precisamos do nosso componente de ordem superior (vamos chamá-lo withHover ) para encapsular o código de processamento de eventos do mouse e depois passar a propriedade hovering para os componentes que ele renderiza. Isso nos permitirá impedir a duplicação do código correspondente, colocando-o no componente withHover .

Em última análise, é isso que queremos alcançar. Sempre que precisarmos de um componente que precise ter uma idéia de sua propriedade hovering , podemos passar esse componente para um componente de ordem superior com o withHover . Ou seja, queremos trabalhar com componentes, como mostrado abaixo.

 const InfoWithHover = withHover(Info) const TrendChartWithHover = withHover(TrendChart) const DailyChartWithHover = withHover(DailyChart) 

Então, quando o que withHover é renderizado, ele será o componente de origem para o qual a propriedade hovering é passada.

 function Info ({ hovering, height }) { return (   <>     {hovering === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) } 

Por uma questão de fato, agora apenas precisamos implementar o componente withHover . Pelo exposto, pode-se entender que ele deve executar três ações:

  • Leve um argumento para Component.
  • Retorne um novo componente.
  • Renderize o argumento Component passando a propriedade hovering para ele.

▍ Aceitando o argumento Component


 function withHover (Component) { } 

EtVoltar a um novo componente


 function withHover (Component) { return class WithHover extends React.Component { } } 

▍ Renderização do componente Component com a propriedade pairando passada para ele


Agora, estamos diante da seguinte pergunta: como chegar à propriedade hovering ? De fato, já escrevemos o código para trabalhar com essa propriedade. Nós apenas precisamos adicioná-lo ao novo componente e, em seguida, passar a propriedade hovering ao renderizar o componente passado para o componente de ordem superior na forma do argumento Component .

 function withHover(Component) { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component hovering={this.state.hovering} />       </div>     );   } } } 

Prefiro falar sobre essas coisas da seguinte maneira (como a documentação do React diz): um componente converte propriedades em uma interface do usuário e um componente de ordem superior converte um componente em outro componente. No nosso caso, transformaremos os componentes Info , TrendChart e DailyChart em novos componentes que, graças à propriedade DailyChart , sabem se o ponteiro do mouse está acima deles.

Notas adicionais


Neste ponto, revisamos todas as informações básicas sobre componentes de ordem superior. No entanto, há algumas coisas mais importantes a serem discutidas.

Se você der uma olhada no nosso HOC withHover , perceberá que ele tem pelo menos um ponto fraco. Isso implica que o componente receptor da propriedade hovering não apresentará nenhum problema com essa propriedade. Na maioria dos casos, é provável que essa suposição seja justificada, mas pode acontecer que isso seja inaceitável. Por exemplo, e se um componente já tiver uma propriedade hovering ? Nesse caso, haverá uma colisão de nomes. Portanto, uma withHover pode ser feita no componente withHover , que permite ao usuário desse componente especificar qual nome a propriedade em movimento passada para os componentes deve ter. Como withHover é apenas uma função, vamos reescrevê-la para que seja withHover segundo argumento que define o nome da propriedade a ser passado para o componente.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

Agora, graças ao mecanismo de parâmetro padrão do ES6, definimos o valor padrão do segundo argumento como hovering , mas se o usuário do componente withHover quiser mudar isso, ele poderá passar, nesse segundo argumento, o nome que ele precisa.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } function Info ({ showTooltip, height }) { return (   <>     {showTooltip === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) } const InfoWithHover = withHover(Info, 'showTooltip') 

Problema com a implementação suspensa


Você pode ter notado outro problema com a implementação do withHover . Se analisarmos nosso componente Info , você notará que ele, entre outras coisas, aceita a propriedade height . A maneira como organizamos tudo agora significa que a height será definida como undefined . O motivo disso é que o componente withHover é o componente responsável por renderizar o que é passado para ele como o argumento Component . Agora não estamos transferindo outras propriedades além da hovering que criamos para o Component Component.

 const InfoWithHover = withHover(Info) ... return <InfoWithHover height="16px" /> 

A propriedade height é passada para o componente InfoWithHover . E qual é esse componente? Este é o componente do qual retornamos withHover .

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     console.log(this.props) // { height: "16px" }     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

Dentro do componente WithHover this.props.height é 16px , mas no futuro não faremos nada com essa propriedade. Precisamos fazer essa propriedade passar para o argumento Component , que estamos renderizando.

 render() {     const props = {       [propName]: this.state.hovering,       ...this.props,     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     ); } 

Sobre os problemas de trabalhar com componentes de terceiros da mais alta ordem


Acreditamos que você já apreciou as vantagens do uso de componentes de ordem superior na reutilização da lógica em vários componentes sem a necessidade de copiar o mesmo código. Agora vamos nos perguntar se há alguma falha nos componentes de ordem superior. Esta pergunta pode ser respondida positivamente, e já encontramos essas deficiências.

Ao usar o HOC, ocorre a inversão do controle . Imagine que estamos usando um componente de ordem superior que não foi desenvolvido por nós, como o HOC withRouter React Router. De acordo com a documentação, withRouter passará as propriedades de match , location e history para o componente que ele empacotou ao renderizá-lo.

 class Game extends React.Component { render() {   const { match, location, history } = this.props // From React Router   ... } } export default withRouter(Game) 

Observe que não estamos criando um elemento Game (ou seja, - <Game /> ). Transferimos totalmente nosso componente React Router e confiamos que este componente não apenas seja renderizado, mas também passará as propriedades corretas para o componente. Já encontramos esse problema antes, quando falamos sobre um possível conflito de nome ao passar a propriedade hovering . Para corrigir isso, decidimos permitir que o withHover HOC withHover usasse o segundo argumento para configurar o nome da propriedade correspondente. Usando o HOC de outra pessoa com o withRouter , não temos essa oportunidade. Se as propriedades de match , location ou history já estiverem sendo usadas no componente Game , podemos dizer que não tivemos sorte. Ou seja, temos que alterar esses nomes em nosso componente ou recusarmos usar o HOC com o withRouter .

Sumário


Falando sobre o HOC no React, há duas coisas importantes a serem lembradas. Primeiro de tudo, o HOC é apenas um padrão. Os componentes de ordem superior nem sequer podem ser chamados de algo específico do React, apesar de estarem relacionados à arquitetura do aplicativo. Segundo, para desenvolver aplicativos React, você não precisa saber sobre componentes de ordem superior. Você pode não estar familiarizado com eles, mas escreva programas excelentes. No entanto, como em qualquer empresa, quanto mais ferramentas você tiver, melhor será o resultado do seu trabalho. E, se você escrever aplicativos usando o React, fará um desserviço a si mesmo sem adicionar HOC ao seu arsenal.

Caros leitores! Você usa componentes de ordem superior no React?

Source: https://habr.com/ru/post/pt428572/


All Articles