O lado feio do React Hooks

Neste post, compartilharei meu próprio ponto de vista sobre React Hooks, e como o título deste post indica, não sou um grande fã.

Vamos analisar a motivação para abandonar as classes em favor dos ganchos, conforme descrito na documentação oficial do React.

Motivação #1: Classes são confusas

descobrimos que classes podem ser uma grande barreira para aprender React. Você tem que entender como funciona em JavaScript, o que é muito diferente de como funciona na maioria das linguagens. Você deve se lembrar de vincular os manipuladores de eventos. Sem propostas de sintaxe instáveis , o código é muito verboso […] A distinção entre os componentes de função e classe no React e quando usar cada um leva a divergências até mesmo entre desenvolvedores experientes do React.

Ok, posso concordar que pode ser um pouco confuso quando você está apenas começando no Javascript, mas as funções de seta resolvem a confusão e chamam um recurso de estágio 3 que já está sendo compatível com o Typecript, uma “sintaxe instável”, é pura demagogia. A equipe do React está se referindo à sintaxe do campo de classe , uma sintaxe que já está sendo amplamente usada e provavelmente em breve terá suporte oficial:

class Foo extends React.Component {
onPress = () => {
console.log(this.props.someProp);
}
render() {
return <Button onPress={this.onPress} />
}
}

Como você pode ver, usando uma função de seta de campo de classe, você não precisa vincular nada no construtor e sempre apontará para o contexto correto.

E se as classes são confusas, o que podemos dizer sobre as novas funções-hook? Uma função-hook não é uma função regular, porque tem estado, tem uma aparência estranha (também conhecida como , e pode ter várias instâncias. Mas definitivamente não é uma classe, é algo no meio, e de agora em diante me referirei para ele como um Funclass . Então, esses Funclasses serão mais fáceis para humanos e máquinas? Não tenho certeza sobre máquinas, mas realmente não acho que Funclasses sejam conceitualmente mais fáceis de entender do que classes. Classes tem conceitos e pensamentos bem conhecidas, e todo desenvolvedor está familiarizado com o conceito de, mesmo que em Javascript seja um pouco diferente. Por outro lado, as Funclasses são um conceito novo e bastante estranho. Eles parecem muito mais mágicos e confiam muito em convenções ao invés de em uma sintaxe estrita. Você tem que seguir algumas regras estranhas, precisa ter cuidado com onde colocar seu código, e existem muitas armadilhas. E estar preparado para algum nomeação terrível como (um nome fantasia para ), , , (quê??) e muito mais.

A sintaxe das classes foi inventada especificamente para lidar com o conceito de múltiplas instâncias e o conceito de escopo de instância (o propósito exato de ). Funclasses são apenas uma forma estranha de atingir o mesmo objetivo, usando as peças erradas do quebra-cabeça. Muitas pessoas estão confundindo Funclasses com programação funcional, mas Funclasses são, na verdade, apenas classes disfarçadas. Uma classe é um conceito, não uma sintaxe.

Ah, e sobre a última nota:

A distinção entre os componentes de função e classe no React e quando usar cada um leva a divergências, mesmo entre desenvolvedores experientes do React

Até agora, a distinção era bem clara — se você precisava de um estado ou métodos de ciclo de vida, você usava uma classe, caso contrário, realmente não importa se você usava uma função ou classe. Pessoalmente, gostei da ideia de que, quando me deparei com um componente de função, posso saber imediatamente que se trata de um “componente burro” sem um estado. Infelizmente, com a introdução do Funclasses, essa não é mais a situação.

Motivação #2: É difícil reutilizar a lógica com estado entre os componentes

O React não oferece uma maneira de “anexar” comportamento reutilizável a um componente (por exemplo, conectando-o a uma loja) … O React precisa de um primitivo melhor para compartilhar lógica com estado.

Não é irônico? O maior problema do React, pelo menos na minha opinião, é que ele não fornece uma solução de gerenciamento de estado pronta para uso, nos deixando com um longo debate sobre como devemos preencher essa lacuna e abrindo uma porta para alguns padrões de design muito ruins, como Redux. Então, depois de anos de frustração, a equipe React finalmente chegou à conclusão de que é difícil compartilhar a lógica stateful entre os componentes … quem poderia ter adivinhado.

De qualquer forma, os ganchos vão melhorar a situação? A resposta é não, realmente. Ganchos não podem funcionar com classes, então, se sua base de código já foi escrita com classes, você ainda precisa de outra maneira de compartilhar lógica com estado. Além disso, os ganchos só resolvem o problema de compartilhamento de lógica por instância, mas se você quiser compartilhar o estado entre várias instâncias, ainda precisará usar armazenamentos e soluções de gerenciamento de estado de terceiros e, como eu disse, se você já os usa, não precisa realmente de ganchos. Portanto, ao invés de lutar contra os sintomas, talvez seja hora do React tomar uma ação e implementar uma ferramenta de gerenciamento de estado adequada para gerenciar o estado global (armazenamentos) e o estado local (por instância) e, assim, eliminar essa lacuna de uma vez por todas.

Motivação #3: Componentes complexos tornam-se difíceis de entender

Freqüentemente, tivemos que manter componentes que começaram simples, mas se transformaram em uma confusão incontrolável de lógica com estado e efeitos colaterais. Cada método de ciclo de vida geralmente contém uma combinação de lógica não relacionada. […] O código mutuamente relacionado que muda junto é dividido, mas o código completamente não relacionado acaba combinado em um único método. […] Ganchos permitem que você divida um componente em funções menores com base em quais partes estão relacionadas (como configurar uma assinatura ou buscar dados), ao invés de forçar uma divisão com base em métodos de ciclo de vida.

Se você já está usando isolamento em módulos, esse argumento quase não é relevante. Vamos ver porque:

class Foo extends React.Component {
componentDidMount() {
doA();
doB();
doC();
}
}

Como você pode ver neste exemplo, possivelmente estamos misturando lógica não relacionada em , mas isso está inchando nosso componente? Não exatamente. Toda a implementação fica fora da classe e o estado fica nos módulos isolados. Sem os armazenamentos, toda a lógica com estado deve ser implementada dentro da classe, e a classe teria sido realmente inchada. Mas, novamente, parece que o React está resolvendo um problema que existe principalmente em um mundo sem ferramentas de gerenciamento de estado. Na realidade, a maioria dos grandes aplicativos já está usando uma ferramenta de gerenciamento de estado, e esse problema já foi mitigado. Além disso, na maioria dos casos, provavelmente poderíamos dividir essa classe em componentes menores e colocar cada um dos subcomponentes.

Com Funclasses, poderíamos escrever algo assim:

função Foo () { 
useA ();
useB ();
useC ();
}

Parece um pouco mais limpo, mas não é? Ainda precisamos escrever 3 ganchos diferentes em algum lugar, então, no final, vamos escrever mais código e veja o que fizemos aqui - com o componente de classe, você pode dizer à primeira vista o que o componente está fazendo na montagem. No exemplo do Funclass, você precisa seguir os ganchos e tentar procurar por um com um array de dependências vazia, a fim de entender o que o componente está fazendo na montagem.

A natureza declarativa dos métodos de ciclo de vida é principalmente uma coisa boa, e achei muito mais difícil investigar o fluxo de Funclasses. Eu vi muitos casos em que o Funclasses tornou mais fácil para os desenvolvedores escreverem códigos ruins, veremos um exemplo mais tarde.

Mas, primeiro, devo admitir que há algo de bom nisso , dê uma olhada no seguinte exemplo:

useEffect(() => {
subscribeToA();
return () => {
unsubscribeFromA();
};
}, []);

O gancho nos permite emparelhar a lógica de assinatura e cancelamento. Este é realmente um padrão muito bom. O mesmo vale para o emparelhamento de componentDidMount e componentDidUpdate. Em minha experiência, esses casos não são tão comuns, mas ainda são casos de uso válidos e é realmente útil aqui. A questão é: por que temos que usar Funclasses para obter ? por que não podemos ter algo semelhante com Classes? A resposta é que podemos:

class Foo extends React.Component {
someEffect = effect((value1, value2) => {
subscribeToA(value1, value2);
return () => {
unsubscribeFromA();
};
})
render(){
this.someEffect(this.props.value1, this.state.value2);
return <Text>Hello world</Text>
}
}

A função memorizará a função fornecida e a chamará novamente apenas se um de seus parâmetros tiver sido alterado. Ao disparar o efeito de dentro de nossa função de renderização, garantimos que ela seja chamada em cada renderização / atualização, mas a função fornecida será executada novamente apenas se um de seus parâmetros tiver sido alterado, então alcançamos resultados semelhantes ao em termos de combinação e , infelizmente, ainda precisamos fazer manualmente a limpeza em . Além disso, é um pouco feio chamar a função de efeito de dentro do render. Para obter exatamente os mesmos resultados, como , o React precisará adicionar suporte para ele.

O resultado final é que não deve ser considerado uma motivação válida para entrar no Funclasses. É um motivo válido por si só e pode ser implementado para as Classes também.

Você pode verificar a implementação da função effect aqui e, se quiser vê-la em ação, verifique este exemplo de trabalho.

Motivação #4: Desempenho

Descobrimos que os componentes Classes podem encorajar padrões não intencionais que fazem essas otimizações retrocederem para um caminho mais lento. As Classes também apresentam problemas para as ferramentas de hoje. Por exemplo, as classes não minimizam muito bem e tornam o hot-reloading fragmentado e pouco confiável

A equipe do React está dizendo que as classes são mais difíceis de otimizar e minimizar e que o Funclasses deve melhorar as coisas de alguma forma. Bem, eu só tenho uma coisa a dizer sobre isso — mostre-me os números.

Não consegui encontrar nenhum papel, ou qualquer aplicativo de demonstração de benchmark que pudesse clonar e executar, comparando o desempenho das Classes vs Funclasses. O fato de não termos visto tal demo não é surpreendente — Funclasses precisam implementar (ou se você preferir) de alguma forma, então eu praticamente espero que os mesmos problemas que tornam as classes difíceis de otimizar afetarão Funclasses também.

De qualquer forma, todo o debate sobre desempenho realmente não vale nada sem mostrar os números, então não podemos realmente usar isso como um argumento.

Motivação # 5: Funclasses são menos verbosas

Você pode encontrar muitos exemplos de redução de código convertendo uma classe em Funclass, mas a maioria, senão todos, os exemplos aproveitam o gancho para combinar e , assim, obtendo grande impacto. Mas, como eu disse antes, não deve ser considerado uma vantagem do Funclass, e se você ignorar a redução de código alcançada por ele, terá um impacto muito menor. E se você tentar otimizar seus Funclasses usando ,e assim por diante, você pode até acabar com um código mais verboso do que uma classe equivalente. Ao comparar componentes pequenos e triviais, Funclasses ganham sem dúvida, porque as classes têm alguns clichês inerentes que você precisa pagar por menor que seja sua classe. Mas ao comparar componentes grandes, você mal consegue ver as diferenças e, às vezes, como eu disse, as classes podem ser ainda mais limpas.

Por último, tenho que dizer algumas palavras sobre : useContext é, na verdade, uma grande melhoria em relação à API de contexto original que temos atualmente para as classes. Mas, novamente, por que não podemos ter essa API limpa e agradável para as classes também? por que não podemos fazer algo assim:

// Em "./someContext" :
export const someContext = React.Context({helloText: 'bla'});
// Em "Foo":
import {someContext} from './someContext';
class Foo extends React.component {
render() {
<View>
<Text>{someContext.helloText}</Text>
</View>
}
}

Quando é alterado no contexto, o componente deve ser renderizado novamente para refletir as alterações. É isso aí. não há necessidade de HOCs feios.

Então, por que a equipe do React escolheu melhorar apenas a API useContext e não a API de contexto regular? Eu não sei. Mas isso não significa que Funclasses sejam inerentemente mais limpos. Tudo isso significa que o React deve fazer um trabalho melhor implementando as mesmas melhorias de API para as classes.

Então, depois de levantar algumas questões sobre as motivações, vamos dar uma olhada em outras coisas que eu não gosto no Funclasses.

O efeito colateral oculto

Uma das coisas que mais me incomoda na implementação do useEffect em Funclasses, é a falta de clareza sobre quais são os efeitos colaterais de um determinado componente. Com as classes, se você quiser descobrir o que um componente está fazendo na montagem, pode facilmente verificar o código ou verificar o construtor. Se você vir uma chamada repetida, provavelmente deveria verificar . Com o novo gancho useEffect, os efeitos colaterais podem ser ocultos e profundamente aninhados no código.

Digamos que detectamos algumas chamadas indesejadas para o servidor. Observamos o código do componente suspeito e vemos o seguinte:

const renderContacts = (props) => {
const [contacts, loadMoreContacts] = useContacts(props.contactsIds);
return (
<SmartContactList contacts={contacts}/>
)
}

Nada de especial aqui. Devemos investigar ou talvez devamos mergulhar ? Vamos mergulhar em :

export const useContacts = (contactsIds) => {
const {loadedContacts, loadingStatus} = useContactsLoader();
const {isRefreshing, handleSwipe} = useSwipeToReresh(loadingStatus);
// ... vários outras funções useX()
useEffect(() => {
//** muito código aqui, tudo relacionado com alguma animação que é relacionada ao carregamento de contatos *//

}, [loadingStatus]);

//...resto do código
}

Ok, está começando a ficar complicado. onde está o efeito colateral oculto? Se mergulharmos , veremos:

export const useSwipeToRefresh = (loadingStatus) => {
// ...mais código aqui
// ...outras linhas de código aqui

useEffect(() => {
if(loadingStatus === 'refresing') {
refreshContacts(); // bingo! nosso side effect escondido!
}
}); // <== nós esquecemos o array de depêndencias!
}

Encontramos nosso efeito oculto. irá buscar contatos acidentalmente em cada renderização de componente. Em uma grande base de código e alguns componentes mal estruturados, useEffect aninhados podem causar problemas sérios.

Não estou dizendo que você não pode escrever código ruim com classes também, mas Funclasses são muito mais propensas a erros e sem a estrutura estritamente definida dos métodos de ciclo de vida, é muito mais fácil fazer coisas ruins.

API inchada

Ao adicionar a API de ganchos ao lado das classes, a API do React é praticamente duplicada. Todo mundo precisa aprender duas metodologias completamente diferentes agora. E devo dizer que a nova API é muito mais obscura que a antiga. Coisas simples, como obter as props e o estado anteriores, estão se tornando um bom material de entrevista. Você pode escrever um gancho para obter os adereços anteriores, sem a ajuda do Google?

Uma grande biblioteca como o React deve ser muito cuidadosa ao adicionar mudanças tão grandes na API, e a motivação aqui não estava nem perto de ser justificada.

Falta de declaratividade

Na minha opinião, Funclasses tendem a ser muito mais confusas do que Classes. Por exemplo, é mais difícil encontrar o ponto de entrada do componente — com as classes, você apenas procura a função , mas com Funclasses pode ser difícil localizar a instrução de retorno principal. Além disso, é mais difícil seguir as diferentes instruções useEffect e entender o fluxo do componente, em oposição aos métodos regulares de ciclo de vida que fornecem algumas boas dicas de onde você precisa procurar seu código. Se estou procurando por algum tipo de lógica de inicialização, vou pular ( cmd + shift + o no VSCode) para . Se estou procurando algum tipo de mecanismo de atualização, provavelmente irei pular para e assim por diante. Com Funclasses, acho muito mais difícil me orientar dentro de grandes componentes.

Vamos acoplar o universo ao React

As pessoas começam a usar bibliotecas específicas do React para fazer coisas simples que são feitas principalmente de lógica pura e podem facilmente ser desconectadas do React. Dê uma olhada neste gancho de localização de rastreamento, por exemplo:

import {useLocation} from 'react-use';const Demo = () => {
const state = useLocation();
return (
<div>
{JSON.stringify(state)}
</div>
);
};

Por que não podemos usar uma biblioteca vanilla pura como esta:

import {tracker} de 'vanilaJsTracker'; const Demo = () => { 
const [local, setLocation] = useState ({});
useEffect () {
tracker.onChange (setLocation);
}, []);
return (
<div>
{JSON.stringify (state)}
</div>
);
};

É mais verboso? Sim. A primeira solução é definitivamente mais curta. Mas a segunda solução é manter o mundo JS desconectado do React, e adicionar mais algumas linhas de código é um pequeno preço a pagar por algo tão importante. Ganchos personalizados abriram uma porta para possibilidades infinitas de acoplar lógica pura ao estado do React, e essas bibliotecas estão se espalhando como um incêndio.

Parece errado

Sabe aquela sensação de que algo não está certo? É assim que me sinto em relação aos ganchos. Às vezes, posso identificar o problema exato, mas às vezes é apenas uma sensação geral de que estamos no caminho errado. Quando você descobre um bom conceito, pode ver como as coisas estão funcionando bem juntas. Mas quando você está lutando com o conceito errado, acontece que você precisa adicionar mais e mais coisas e regras específicas para fazer as coisas funcionarem. Com os ganchos, há mais e mais coisas estranhas que aparecem, ganchos mais “úteis” que ajudam você a fazer algumas coisas triviais e mais coisas para aprender. Se precisamos de tantos utilitários para o nosso trabalho do dia a dia, apenas para esconder algumas complicações estranhas, este é um grande sinal de que estamos no caminho errado.

Há poucos anos, quando mudei do Angular 1.5 para o React, fiquei surpreso com a simplicidade da API do React e como a documentação era curta. O Angular costumava ter documentação enormes. Você levaria dias para cobrir tudo — o mecanismo de resumo, as diferentes fases de compilação, transcluir, vincular, modelos e muito mais. Isso por si só foi uma grande indicação para mim de que algo está errado. React, por outro lado, imediatamente pareceu certo. Era limpo e conciso, você poderia revisar toda a documentação em questão de horas e estava pronto para começar. Enquanto tentava ganchos pela primeira vez, e pela segunda vez, e por todas as vezes que se seguiram, me vi obrigado a voltar a documentação repetidas vezes.

Uma nota importante

Depois de ler alguns dos comentários, descobri que muitas pessoas pensam que sou um defensor da classe. Bem, está longe de ser verdade.

As Classes têm muitas desvantagens, mas as Funclasses são as piores. Como já afirmei no início do artigo — Classe é um conceito, não uma sintaxe. Lembra daquela sintaxe de protótipo horrível que estava atingindo o mesmo objetivo das Classes, mas da maneira mais estranha? Então é isso que eu sinto sobre Funclasses. Você não precisa amar Classes para odiar a sintaxe do antigo protótipo, e não precisa amar Classes para odiar Funclasses :)

Não é uma briga entre OOP e programação funcional, porque Funclasses não está relacionado à programação funcional de forma alguma, e estritamente falando, escrever um aplicativo com React, não importa se você usa Classes ou não, não é exatamente OOP.

Conclusão

Eu odeio ser o estraga-prazeres, mas eu realmente acho que Hooks pode ser a segunda pior coisa que aconteceu à comunidade React (o primeiro lugar ainda é ocupado por Redux). Acrescentou outro debate inútil a um ecossistema já frágil, não está claro agora se os ganchos são o caminho recomendado ou se é apenas mais uma característica e uma questão de gosto pessoal.

Espero que a comunidade React acorde e peça paridade entre as funcionalidades do Funclasses e classes. Podemos ter uma API de contexto melhor em classes e podemos ter algo como useEffect para classes. O React deve nos dar a escolha de continuar usando classes se quisermos, e não eliminá-lo à força adicionando mais recursos apenas para Funclasses, deixando as classes para trás.

Aliás, já no final de 2017, publiquei um post com o título “O lado feio do Redux”, e hoje até Dan Abramov, o criador do Redux, já admite que Redux foi um grande erro:

Image for post
Image for post

https://mobile.twitter.com/dan_abramov/status/1191495127358935040

É tudo apenas a história se repetindo? O tempo vai dizer.

De qualquer forma, eu e meus companheiros de equipe decidimos ficar com as Classes por enquanto e uma solução baseada em Mobx como ferramenta de gerenciamento de estado. Eu acho que há uma grande diferença na popularidade dos Hooks entre desenvolvedores solo e aqueles que trabalham em equipe — a natureza ruim dos Hooks é muito mais visível em uma grande base de código, onde você precisa lidar com o código de outras pessoas. Pessoalmente, eu realmente gostaria que o React pudesse apenas em todo esse capítulo de Hooks.

Vou começar a trabalhar em um RFC que irá sugerir uma solução de gerenciamento de estado simples, limpa e integrada para React, que resolverá o problema de compartilhamento de lógica com estado de uma vez por todas, espero que de uma forma menos complicada do que Funclasses.

Image for post
Image for post

Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store