Qual a diferença entre os componentes funcionais do React e os componentes baseados em classe?

Qual a diferença entre os componentes funcionais do React e os componentes baseados em classe? Já há algum tempo, a resposta tradicional a essa pergunta é: "O uso de classes permite que você use um grande número de recursos de componentes, por exemplo, estado". Agora, com o advento dos ganchos , essa resposta não reflete mais o verdadeiro estado das coisas.

Você pode ter ouvido falar que um desses tipos de componentes tem melhor desempenho que o outro. Mas qual? A maioria dos benchmarks que testam isso tem falhas , então eu tiraria conclusões com base em seus resultados com muita cautela. O desempenho depende principalmente do que acontece no código, e não se componentes funcionais ou componentes baseados em classe são selecionados para implementar determinados recursos. Nosso estudo mostrou que a diferença de desempenho entre os diferentes tipos de componentes é insignificante. No entanto, deve-se notar que as estratégias de otimização usadas para trabalhar com elas diferem um pouco.



De qualquer forma, não recomendo reescrever componentes existentes usando novas tecnologias, se não houver boas razões para isso, e se você não se importar de estar entre aqueles que começaram a usar essas tecnologias antes de todos os outros. Os ganchos ainda são uma nova tecnologia (a mesma que a biblioteca React foi em 2014), e algumas “práticas recomendadas” para sua aplicação ainda não foram incluídas nos manuais do React.

Para onde finalmente chegamos? Existem diferenças fundamentais entre os componentes funcionais do React e os componentes baseados em classes? Claro, existem essas diferenças. Essas são diferenças no modelo mental do uso de tais componentes. Neste artigo, considerarei a diferença mais séria. Existe desde que, em 2015, os componentes funcionais apareceram, mas geralmente é ignorado. Consiste no fato de que os componentes funcionais capturam valores renderizados. Vamos falar sobre o que isso realmente significa.

Note-se que este material não constitui uma tentativa de avaliar componentes de diferentes tipos. Acabei de descrever a diferença entre os dois modelos de programação no React. Se você quiser saber mais sobre o uso de componentes funcionais à luz das inovações, consulte esta lista de perguntas e respostas sobre ganchos.

Quais são os recursos do código de componentes com base em funções e classes?


Considere este componente:

function ProfilePage(props) {  const showMessage = () => {    alert('Followed ' + props.user);  };  const handleClick = () => {    setTimeout(showMessage, 3000);  };  return (    <button onClick={handleClick}>Follow</button>  ); } 

Ele exibe um botão que, pressionando a função setTimeout , simula uma solicitação de rede e exibe uma caixa de mensagem confirmando a operação. Por exemplo, se ' props.user 'Dan' estiver armazenado em props.user , na janela de mensagens, após três segundos, 'Followed Dan' será exibido.

Observe que não importa se funções de seta ou declarações de função são usadas aqui. Uma construção da function handleClick() formulário function handleClick() funcionará exatamente da mesma maneira.

Como reescrever esse componente como uma classe? Se você apenas refizer o código examinado, convertendo-o no código de um componente com base em uma classe, obterá o seguinte:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; handleClick = () => {   setTimeout(this.showMessage, 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

É geralmente aceito que dois desses fragmentos de código são equivalentes. E os desenvolvedores geralmente são completamente livres, no curso da refatoração de código, se transformam em outro, sem pensar nas possíveis consequências.


Esses trechos de código parecem equivalentes

No entanto, há uma pequena diferença entre esses trechos de código. Dê uma olhada neles. Veja a diferença? Por exemplo, eu não a vi imediatamente.

Além disso, consideraremos essa diferença, portanto, para aqueles que desejam entender a essência do que está acontecendo, como um exemplo prático desse código.

Antes de continuarmos, gostaria de enfatizar que a diferença em questão não tem nada a ver com os ganchos do React. Nos exemplos anteriores, a propósito, ganchos nem sequer são usados. É sobre a diferença entre funções e classes no React. E se você planeja usar muitos componentes funcionais em seus aplicativos React, convém entender essa diferença.

De fato, ilustraremos a diferença entre funções e classes pelo exemplo de um erro frequentemente encontrado nos aplicativos React.

O erro comum nos aplicativos React.


Abra a página de exemplo que exibe uma lista que permite selecionar perfis de usuário e dois botões Follow exibidos pelos ProfilePageClass e ProfilePageClass , funcionais e baseados na classe, cujo código é mostrado acima.

Tente, para cada um desses botões, executar a seguinte sequência de ações:

  1. Clique no botão
  2. Altere o perfil selecionado antes de decorridos 3 segundos após clicar no botão.
  3. Leia o texto exibido na caixa de mensagem.

Feito isso, você notará os seguintes recursos:

  • Quando você clica no botão formado pelo componente funcional com o perfil Dan selecionado e depois muda para o perfil Sophie , 'Followed Dan' será exibido na caixa de mensagem.
  • Se você fizer o mesmo com um botão formado por um componente com base em uma classe, 'Followed Sophie' será exibida.


Recursos de componentes baseados em classe

Neste exemplo, o comportamento do componente funcional está correto. Se eu me inscrevi no perfil de alguém e depois mudei para outro perfil, meu componente não deve duvidar de qual perfil eu me inscrevi. Obviamente, a implementação do mecanismo em questão com base no uso de classes contém um erro (a propósito, você definitivamente deve se tornar um assinante do Sofia ).

Causas de mau funcionamento de componentes com base em classe


Por que um componente baseado em classe se comporta dessa maneira? Para entender isso, vamos dar uma olhada no método showMessage em nossa classe:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; 

Este método lê dados deste this.props.user . As propriedades no React são imutáveis, portanto não são alteradas. No entanto, this , como sempre, é uma entidade mutável.

De fato, o objetivo de ter this em uma classe reside na capacidade de mudar isso. A própria biblioteca do React realiza periodicamente this mutações, o que permite trabalhar com as versões mais recentes do método render e dos métodos do ciclo de vida dos componentes.

Como resultado, se o nosso componente for renderizado novamente durante a execução da solicitação, o this.props será alterado. Depois disso, o método showMessage lerá o valor do user da entidade de props "muito nova".

Isso permite que você faça uma observação interessante sobre as interfaces do usuário. Se dissermos que a interface do usuário, conceitualmente, é uma função do estado atual do aplicativo, os manipuladores de eventos fazem parte dos resultados da renderização - assim como os resultados visíveis da renderização. Nossos manipuladores de eventos "pertencem" a uma operação de renderização específica, juntamente com propriedades e estados específicos.

No entanto, agendar um tempo limite cujo retorno de chamada this.props lê viola essa conexão. O showMessage showMessage não showMessage "vinculado" a nenhuma operação de renderização específica; como resultado, "perde" as propriedades corretas. A leitura de dados this interrompe esta conexão.

Como, por meio de componentes baseados em classe, resolver o problema?


Imagine que não há componentes funcionais no React. Como então resolver este problema?

Precisamos de algum mecanismo para "restaurar" a conexão entre o método render com as propriedades corretas e o showMessage showMessage, que lê dados das propriedades. Esse mecanismo deve estar localizado em algum lugar onde a essência dos props com os dados corretos é perdida.

Uma maneira de fazer isso é ler this.props antecipadamente no manipulador de eventos e, em seguida, passar explicitamente o que foi lido para a função de retorno de chamada usada em setTimeout :

 class ProfilePage extends React.Component { showMessage = (user) => {   alert('Followed ' + user); }; handleClick = () => {   const {user} = this.props;   setTimeout(() => this.showMessage(user), 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

Essa abordagem está funcionando . Mas as construções adicionais usadas aqui, com o tempo, levarão a um aumento no volume do código e ao fato de que a probabilidade de erros nele aumentará. E se precisarmos de mais do que uma única propriedade? E se também precisarmos trabalhar com o estado? Se o método showMessage outro método e esse método ler this.props.something ou this.state.something , encontraremos o mesmo problema novamente. E, para resolvê-lo, teríamos que passar this.props e this.state como argumentos para todos os métodos chamados de showMessage .

Se for verdade, isso destruirá todas as conveniências que o uso de componentes com base em classes oferece. É difícil lembrar que trabalhar com métodos dessa maneira é difícil, é difícil automatizar, como resultado, os desenvolvedores geralmente, em vez de usar métodos semelhantes, concordam que há erros em seus projetos.

Da mesma forma, a incorporação do código de alert no handleClick não resolve um problema mais global. Precisamos estruturar o código para que ele possa ser dividido em vários métodos, mas também para que possamos ler as propriedades e o estado que correspondem à operação de renderização associada a uma chamada específica. A propósito, esse problema nem se aplica exclusivamente ao React. Você pode reproduzi-lo em qualquer biblioteca para desenvolver interfaces de usuário, que coloca dados em objetos mutáveis ​​como this .

Talvez, para resolver esse problema, você possa vincular métodos a this no construtor?

 class ProfilePage extends React.Component { constructor(props) {   super(props);   this.showMessage = this.showMessage.bind(this);   this.handleClick = this.handleClick.bind(this); } showMessage() {   alert('Followed ' + this.props.user); } handleClick() {   setTimeout(this.showMessage, 3000); } render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

Mas isso não resolve o nosso problema. Lembre-se de que estamos lendo os dados this.props tarde demais, e não na sintaxe usada! No entanto, esse problema será resolvido se confiarmos no fechamento do JavaScript.

Os desenvolvedores geralmente tentam evitar fechamentos, pois não é fácil pensar em valores que, com o tempo, não podem sofrer mutações. Mas as propriedades do React são imutáveis! (Ou, no mínimo, isso é altamente recomendado). Isso permite que você pare de perceber os fechamentos como algo pelo qual o programador pode, como eles dizem, "dar um tiro no próprio pé".

Isso significa que, se você "bloquear" as propriedades ou o estado de uma operação de renderização específica no fechamento, sempre poderá contar com eles para não serem alterados.

 class ProfilePage extends React.Component { render() {   //  !   const props = this.props;   //    ,      render.   //   -   .   const showMessage = () => {     alert('Followed ' + props.user);   };   const handleClick = () => {     setTimeout(showMessage, 3000);   };   return <button onClick={handleClick}>Follow</button>; } } 

Como você pode ver, aqui "capturamos" as propriedades durante a chamada para o método render .


Propriedades capturadas pela chamada de renderização

Com essa abordagem, é garantido que qualquer código que esteja no método de render (incluindo showMessage ) veja as propriedades capturadas durante uma chamada específica para esse método. Como resultado, o React não será mais capaz de nos impedir de fazer o que precisamos.

No método de render , você pode descrever quantas funções auxiliares desejar e todas elas poderão usar as propriedades e o estado “capturados”. Foi assim que o fechamento resolveu nosso problema.

Análise da solução do problema usando o fechamento


O que acabamos de chegar nos permite resolver o problema , mas esse código parece estranho. Por que uma classe é necessária, se as funções são declaradas dentro do método render , e não como métodos de classe?

De fato, podemos simplificar esse código se livrando do "shell" na forma de uma classe que o rodeia:

 function ProfilePage(props) { const showMessage = () => {   alert('Followed ' + props.user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

Aqui, como no exemplo anterior, as propriedades são capturadas na função, pois o React as transmite a ela como argumento. Diferente this , o React nunca props objeto de props .

Isso se torna um pouco mais óbvio se os props destruídos na declaração da função:

 function ProfilePage({ user }) { const showMessage = () => {   alert('Followed ' + user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

Quando o componente pai ProfilePage com outras propriedades, o React chama a função ProfilePage . Mas o manipulador de eventos que já foi chamado pertence à chamada anterior dessa função, esta chamada usa seu próprio valor de user e seu próprio showMessage showMessage, que lê esse valor. Tudo isso permanece intocado.

É por isso que, na versão original do nosso exemplo, ao trabalhar com um componente funcional, selecionar outro perfil depois de clicar no botão correspondente antes da exibição da mensagem não muda nada. Se um perfil da Sophie foi selecionado antes de clicar no botão, 'Followed Sophie' será exibida na janela de mensagem, aconteça o que acontecer.


Usando um componente funcional

Esse comportamento está correto ( a propósito, você também pode se inscrever no Sunil ).

Agora descobrimos qual é a grande diferença entre funções e classes no React. Como já mencionado, estamos falando do fato de que componentes funcionais capturam valores. Agora vamos falar sobre ganchos.

Hooks


Ao usar ganchos, o princípio de "capturar valores" se estende ao estado. Considere o seguinte exemplo:

 function MessageThread() { const [message, setMessage] = useState(''); const showMessage = () => {   alert('You said: ' + message); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value); }; return (   <>     <input value={message} onChange={handleMessageChange} />     <button onClick={handleSendClick}>Send</button>   </> ); } 

Aqui você pode experimentar com ele

Embora este não seja um exemplo exemplar de uma interface de aplicativo de mensagens, este projeto ilustra a mesma idéia: se um usuário enviou uma mensagem, o componente não deve ficar confuso sobre qual mensagem foi enviada. A constante de message desse componente funcional captura o estado que "pertence" ao componente que torna o navegador o manipulador de cliques do botão que ele chama. Como resultado, a message armazena o que estava no campo de entrada no momento do clique no botão Send .

O problema de capturar propriedades e estados por componentes funcionais


Sabemos que os componentes funcionais no React, por padrão, capturam propriedades e estados. Mas e se precisarmos ler os dados mais recentes de propriedades ou estados que não pertencem a uma chamada de função específica? E se quisermos “ lê-los do futuro ”?

Nos componentes baseados em classe, isso pode ser feito simplesmente referindo-se a this.props ou this.state , pois é uma entidade mutável. Sua mudança está envolvida no React. Os componentes funcionais também podem trabalhar com valores mutáveis ​​que são compartilhados por todos os componentes. Esses valores são chamados ref :

 function MyComponent() { const ref = useRef(null); //     `ref.current`. // ... } 

No entanto, o programador precisa gerenciar esses valores independentemente.

A essência de ref desempenha o mesmo papel que os campos de uma instância de uma classe. Esta é uma "saída de emergência" para um mundo imperativo mutável. Você pode estar familiarizado com o conceito de referências DOM, mas essa ideia é muito mais geral. Pode ser comparado a uma caixa na qual um programador pode colocar alguma coisa.

Mesmo externamente, uma construção da forma this.something parece uma imagem espelhada da construção de this.something . Eles são uma representação do mesmo conceito.

Por padrão, o React não cria entidades ref nos componentes funcionais para os valores mais recentes de propriedade ou estado. Em muitos casos, você não precisará deles, e sua criação automática seria uma perda de tempo. No entanto, o trabalho com eles, se necessário, pode ser organizado por conta própria:

 function MessageThread() { const [message, setMessage] = useState(''); const latestMessage = useRef(''); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value);   latestMessage.current = e.target.value; }; 

Se lermos a message em showMessage , veremos a mensagem que estava no campo no momento do clique no botão Send . Mas se você ler latestMessage.current , poderá obter o valor mais recente - mesmo se continuarmos inserindo texto no campo depois de clicar no botão Send .

Você pode comparar este e este exemplo para avaliar independentemente a diferença. O valor de ref é uma maneira de "evitar" a uniformidade da renderização; em alguns casos, pode ser muito útil.

Em geral, você deve evitar ler ou escrever valores ref durante o processo de renderização, porque esses valores são mutáveis. Nós nos esforçamos para tornar a renderização previsível. No entanto, se precisarmos obter o valor mais recente de algo armazenado nas propriedades ou no estado, atualizar manualmente o valor ref pode ser uma tarefa tediosa. Pode ser automatizado usando o efeito:

 function MessageThread() { const [message, setMessage] = useState(''); //    . const latestMessage = useRef(''); useEffect(() => {   latestMessage.current = message; }); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; 

Aqui está um exemplo que usa esse código

Estamos atribuindo um valor dentro do efeito, como resultado, o valor de ref será alterado somente após a atualização do DOM. Isso garante que nossa mutação não interrompa recursos como Time Slicing e Suspense , que dependem da continuidade das operações de renderização.

Usar o valor ref dessa maneira geralmente não é necessário. A captura de propriedades ou estados geralmente parece ser um padrão muito melhor do comportamento padrão do sistema. No entanto, isso pode ser conveniente ao trabalhar com APIs imperativas , como aquelas que usam intervalos ou assinaturas. Lembre-se de que você pode trabalhar dessa maneira com qualquer valor - com propriedades, com variáveis ​​armazenadas em state, com todo o objeto props ou mesmo com uma função.

Além disso, esse padrão pode ser útil para fins de otimização. Por exemplo, quando algo como useCallback muda com muita frequência. É verdade que a solução preferida é frequentemente usar um redutor .

Sumário


Neste artigo, analisamos um dos padrões errados para o uso de componentes baseados em classe e falamos sobre como resolver esse problema com fechamentos. No entanto, você pode perceber que, ao tentar otimizar ganchos especificando uma matriz de dependências, poderá encontrar erros relacionados a fechamentos obsoletos. Isso significa que as falhas em si são um problema. Eu acho que não.

Como mostrado acima, os fechamentos, de fato, nos ajudam a resolver pequenos problemas difíceis de perceber. Eles também facilitam a gravação de código que funciona corretamente em paralelo . Isso é possível devido ao fato de que dentro do componente as propriedades e o estado corretos com os quais esse componente foi renderizado estão "bloqueados".

Em todos os casos que vi até agora, o problema de "fechamentos obsoletos" ocorreu devido à suposição errônea de que "funções não mudam" ou que "propriedades sempre permanecem as mesmas". Espero que, depois de ler este material, você esteja convencido de que não é assim.

As funções “capturam” suas propriedades e estado - e, portanto, a compreensão de quais funções estão em questão também é importante. Isso não é um erro, é um recurso dos componentes funcionais. As funções não devem ser excluídas da "matriz de dependências" para useEffect ou useCalback , por exemplo. (Uma ferramenta adequada para resolver o problema é geralmente useReducer ou useRef . Falamos sobre isso acima e em breve prepararemos materiais que serão dedicados à escolha dessa ou daquela abordagem).

Se a maior parte do código em nossos aplicativos será baseada em componentes funcionais, isso significa que precisamos saber mais sobre otimização de código e quais valores podem mudar ao longo do tempo.

: « , , , , , ».

. , React , . , « », . , React .

, , .


React —

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


All Articles