Revolução ou dor? Relatório de ganchos de reação do Yandex

Meu nome é Artyom Berezin, sou desenvolvedor de vários serviços internos Yandex. Nos últimos seis meses, tenho trabalhado ativamente com o React Hooks. No processo, houve algumas dificuldades que tiveram que ser combatidas. Agora, quero compartilhar essa experiência com você. No relatório, examinei a API do React Hook de um ponto de vista prático - por que precisamos de ganchos, vale a pena mudar, o que é melhor considerar ao portar. É fácil cometer erros durante a transição, mas evitá-los também não é tão difícil.



- Ganchos são apenas outra maneira de descrever a lógica de seus componentes. Permite adicionar aos componentes funcionais alguns recursos que antes eram inerentes apenas aos componentes nas classes.



Primeiro de tudo, é suporte para o estado interno, então - suporte para efeitos colaterais. Por exemplo - solicitações de rede ou solicitações ao WebSocket: assinatura, cancelamento de assinatura de alguns canais. Ou, talvez, estamos falando de solicitações para outras APIs de navegador assíncronas ou síncronas. Além disso, os ganchos nos dão acesso ao ciclo de vida do componente, ao seu início de vida, ou seja, à montagem, à atualização de seus adereços e à sua morte.



Provavelmente, a maneira mais fácil de ilustrar em comparação. Aqui está o código mais simples que pode estar apenas com um componente nas classes. O componente está mudando alguma coisa. Este é um contador regular que pode ser aumentado ou diminuído, apenas um campo no estado. Em geral, acho que se você estiver familiarizado com o React, o código é completamente óbvio para você.



Um componente semelhante que executa exatamente a mesma função, mas escrito em ganchos, parece muito mais compacto. De acordo com meus cálculos, em média, ao transferir de componentes em classes para componentes em ganchos, o código diminui cerca de uma vez e meia e é o que agrada.

Algumas palavras sobre como os ganchos funcionam. Um gancho é uma função global que é declarada dentro do React e é chamada toda vez que um componente é renderizado. O React rastreia as chamadas para essas funções e pode mudar seu comportamento ou decidir o que deve retornar.



Existem algumas restrições no uso de ganchos que os diferenciam das funções comuns. Antes de tudo, eles não podem ser usados ​​em componentes em classes, apenas uma restrição se aplica porque eles não foram criados para eles, mas para componentes funcionais. Ganchos não podem ser chamados dentro de funções internas, dentro de loops, condições. Somente no primeiro nível de aninhamento, dentro das funções do componente. Essa restrição é imposta pelo próprio React para poder rastrear quais ganchos foram chamados. E ele as empilha em uma certa ordem em seu cérebro. Então, se essa ordem mudar repentinamente ou se alguma desaparecer, serão possíveis erros complexos, esquivos e difíceis de depurar.

Mas se você tem uma lógica bastante complicada e gostaria de usar, por exemplo, ganchos dentro de ganchos, provavelmente isso é um sinal de que você deve fazer um gancho. Suponha que você faça vários ganchos conectados entre si em um gancho personalizado separado. E dentro dele, você pode usar outros ganchos personalizados, criando assim uma hierarquia de ganchos, destacando a lógica geral lá.



Ganchos oferecem algumas vantagens sobre as classes. Primeiro de tudo, como segue o anterior, usando ganchos personalizados, você pode atrapalhar a lógica muito mais facilmente. Anteriormente, usando a abordagem com componentes de ordem superior, estabelecemos algum tipo de lógica compartilhada, e era um invólucro sobre o componente. Agora colocamos essa lógica dentro de ganchos. Assim, a árvore de componentes é reduzida: seu aninhamento é reduzido e torna-se mais fácil para o React rastrear alterações de componentes, recalcular a árvore, recalcular o DOM virtual etc. Isso resolve o problema do chamado wrapper-hell. Aqueles que trabalham com Redux, eu acho, estão familiarizados com isso.

O código escrito usando ganchos é muito mais fácil de minimizar com minimizadores modernos como o Terser ou o UglifyJS antigo. O fato é que não precisamos salvar os nomes dos métodos, não precisamos pensar em protótipos. Após a transpilação, se o alvo for ES3 ou ES5, geralmente obtemos vários protótipos que corrigem. Aqui tudo isso não precisa ser feito, portanto, é mais fácil minimizar. E, como resultado do não uso de classes, não precisamos pensar sobre isso. Para iniciantes, esse costuma ser um grande problema e, provavelmente, uma das principais razões para erros: esquecemos que isso pode ser uma janela, que precisamos vincular um método, por exemplo, no construtor ou de alguma outra maneira.

Além disso, o uso de ganchos permite destacar a lógica que controla qualquer efeito colateral. Anteriormente, essa lógica, especialmente quando temos vários efeitos colaterais para um componente, tinha que ser dividida em diferentes métodos do ciclo de vida do componente. E, como apareceram os ganchos de minimização, o React.memo apareceu, agora os componentes funcionais se prestam à memorização, ou seja, esse componente não será recriado ou atualizado conosco se seus props não forem alterados. Isso não podia ser feito antes, agora é possível. Todos os componentes funcionais podem ser agrupados em memorando. Também dentro do gancho useMemo apareceu, que podemos usar para calcular alguns valores pesados ​​ou instanciar algumas classes de utilitário apenas uma vez.

O relatório ficará incompleto se eu não falar sobre alguns ganchos básicos. Primeiro de tudo, esses são ganchos de gerenciamento de estado.



Primeiro de tudo - useState.



Um exemplo é semelhante ao do início do relatório. useState é uma função que recebe um valor inicial e retorna uma tupla do valor atual e da função para alterar esse valor. Toda a magia é servida pelo React internamente. Podemos simplesmente ler esse valor ou alterá-lo.

Diferentemente das classes, podemos usar quantos objetos de estado forem necessários, dividir o estado em partes lógicas para não misturá-los em um único objeto, como nas classes. E essas peças serão completamente isoladas uma da outra: elas podem ser alteradas independentemente uma da outra. O resultado, por exemplo, deste código: alteramos duas variáveis, calculamos o resultado e exibimos os botões que nos permitem alterar a primeira variável aqui e ali, e a segunda variável aqui e ali. Lembre-se deste exemplo, porque mais tarde faremos uma coisa semelhante, mas muito mais complicada.



Existe um uso desses esteróides para os amantes de Redux. Permite alterar o estado de maneira mais consistente usando um redutor. Eu acho que aqueles que estão familiarizados com o Redux nem conseguem explicar, para aqueles que não estão familiarizados, eu direi.

Um redutor é uma função que aceita um estado e algum objeto, geralmente chamado ação, que descreve como esse estado deve mudar. Mais precisamente, ele passa alguns parâmetros e, dentro do redutor, já decide, dependendo de seus parâmetros, como o estado mudará e, como resultado, um novo estado deve ser retornado, atualizado.



Aproximadamente dessa maneira, é usado no código do componente. Temos um gancho useReducer, ele assume uma função redutora e o segundo parâmetro é o valor inicial do estado. Retorna, como useState, o estado atual e a função para alterá-lo é despachado. Se você passar um objeto de ação para despachar, chamaremos uma mudança de estado.



Gancho de uso muito importante. Permite adicionar efeitos colaterais ao componente, oferecendo uma alternativa ao ciclo de vida. Neste exemplo, usamos um método simples com useEffect: ele está apenas solicitando alguns dados do servidor, com a API, por exemplo, e exibindo esses dados na página.



UseEffect possui um modo avançado; é quando a função transmitida para useEffect retorna outra função; essa função será chamada no próximo loop, quando esse useEffect será aplicado.

Esqueci de mencionar, useEffect é chamado de forma assíncrona, logo após a alteração ser aplicada ao DOM. Ou seja, garante que será executado após a renderização do componente e poderá levar à próxima renderização se alguns valores forem alterados.



Aqui nos encontramos pela primeira vez com um conceito como dependências. Alguns ganchos - useEffect, useCallback, useMemo - aceitam uma matriz de valores como segundo argumento, o que nos permitirá dizer o que rastrear. Alterações nessa matriz levam a algum tipo de efeito. Por exemplo, aqui, hipoteticamente, temos algum tipo de componente para escolher um autor de alguma lista. E um prato com livros deste autor. E quando o autor mudar, useEffect será chamado. Quando esse authorId for alterado, uma solicitação será chamada e os livros serão carregados.

Menciono também que, ao passar ganchos como useRef, essa é uma alternativa ao React.createRef, algo semelhante ao useState, mas as alterações no ref não levam à renderização. Às vezes, conveniente para alguns hacks. useImperativeHandle nos permite declarar certos "métodos públicos" no componente. Se você usar useRef no componente pai, poderá usar esses métodos. Para ser sincero, tentei uma vez para fins educacionais, na prática não foi útil. useContext é apenas uma coisa boa, pois permite que você pegue o valor atual do contexto se o provedor definiu esse valor em algum lugar mais alto no nível da hierarquia.

Há uma maneira de otimizar os aplicativos React nos ganchos: a memorização. A memorização pode ser dividida em interna e externa. Primeiro sobre o exterior.



Isso é React.memo, praticamente uma alternativa à classe React.PureComponent, que rastreia as alterações nos adereços e os componentes alterados apenas quando os adereços ou o estado são alterados.

Aqui, uma coisa semelhante, no entanto, sem um estado. Ele também monitora as alterações nos objetos e, se os objetos foram alterados, ocorre um renderizador. Se os objetos não foram alterados, o componente não é atualizado e economizamos nisso.



Métodos internos de otimização. Primeiro de tudo, isso é algo de baixo nível - useMemo, raramente usado. Ele permite calcular algum valor e recalculá-lo apenas se os valores especificados nas dependências tiverem sido alterados.



Há um caso especial de useMemo para uma função chamada useCallback. Ele é usado principalmente para memorizar o valor das funções do manipulador de eventos que serão passadas aos componentes filhos, para que esses componentes filhos não possam ser renderizados novamente. É usado simplesmente. Descrevemos uma determinada função, envolvemos-na em useCallback e indicamos de quais variáveis ​​ela depende.

Muitas pessoas têm uma pergunta, mas precisamos disso? Precisamos de ganchos? Estamos nos mudando ou ficando como antes? Não existe uma resposta única, tudo depende das preferências. Primeiro de tudo, se você está diretamente rigidamente ligado à programação orientada a objetos, se seus componentes estão acostumados a ela como uma classe, eles têm métodos que podem ser extraídos; então, provavelmente, isso pode parecer supérfluo para você. Em princípio, me pareceu quando ouvi pela primeira vez sobre ganchos que, de alguma forma, era muito complicado demais, algum tipo de mágica estava sendo adicionada e não estava claro o porquê.

Para os amantes de funcionalidades, isso é, digamos, obrigatório, porque ganchos são funções e técnicas de programação funcional são aplicáveis ​​a elas. Por exemplo, você pode combiná-los ou fazer qualquer coisa, usando, por exemplo, bibliotecas como Ramda e similares.



Desde que nos livramos das classes, não precisamos mais vincular esse contexto aos métodos. Se você usar esses métodos como retornos de chamada. Geralmente, isso era um problema, porque era necessário lembrar de vinculá-los ao construtor ou usar uma extensão não oficial da sintaxe da linguagem, como funções de seta como propriedade. Prática bastante comum. Eu usei meu decorador, que também é, em princípio, experimentalmente, em métodos.



Há uma diferença em como o ciclo de vida funciona, como gerenciá-lo. Os ganchos associam quase todas as ações do ciclo de vida ao gancho useEffect, que permite que você assine o nascimento e a atualização de um componente e sua morte. Nas classes, para isso, tivemos que redefinir vários métodos, como componentDidMount, componentDidUpdate e componentWillUnmount. Além disso, o método shouldComponentUpdate agora pode ser substituído pelo React.memo.



Há uma diferença bastante pequena em como o estado é tratado. Primeiro, as classes têm um objeto de estado. Tivemos que enfiar qualquer coisa lá. Em ganchos, podemos dividir o estado lógico em algumas partes, o que seria conveniente para nós operar separadamente.

setState () de componentes em classes com permissão para especificar um patch de estado, alterando um ou mais campos do estado. Em termos gerais, temos que mudar todo o estado como um todo, e isso é até bom, porque está na moda usar todo tipo de coisas imutáveis ​​e nunca esperar que nossos objetos sofram mutações. Eles são sempre novos conosco.

A principal característica das classes que ganchos não têm: poderíamos assinar alterações de estado. Ou seja, alteramos o estado e imediatamente assinamos suas alterações, processando algo imperativamente imediatamente após as alterações serem aplicadas. Em ganchos, isso simplesmente não funciona. Isso precisa ser feito de uma maneira muito interessante, vou lhe dizer mais adiante.

E um pouco sobre a maneira funcional de atualização. Ele funciona lá e ali, quando as funções de mudança de estado aceitam outra função, que esse estado não deve mudar, mas criar. E se, no caso do componente de classe, ele pode retornar algum tipo de patch para nós, então nos ganchos devemos retornar todo o novo valor.

Em geral, é improvável que você obtenha uma resposta para mover-se ou não. Mas aconselho pelo menos tentar, pelo menos pelo novo código, senti-lo. Quando comecei a trabalhar com ganchos, identifiquei imediatamente vários ganchos personalizados que são convenientes para mim no meu projeto. Basicamente, tentei substituir alguns dos recursos que havia implementado por meio dos componentes de ordem superior.



useDismounted - para aqueles que estão familiarizados com o RxJS, há a oportunidade de cancelar massivamente a inscrição de todos os Observáveis ​​em um único componente ou em uma única função, inscrevendo cada Observável em um objeto especial, Assunto e, quando fechado, todas as assinaturas são canceladas. Isso é muito conveniente se o componente for complexo, se houver muitas operações assíncronas dentro do Observable, é conveniente cancelar a assinatura de todas de uma vez, e não de cada uma separadamente.

useObservable retorna um valor de Observable quando um novo aparece lá. Um gancho useBehaviourSubject semelhante retorna de BehaviourSubject. Sua diferença em relação ao Observable é que ele inicialmente tem algum significado.

O conveniente gancho personalizado useDebounceValue nos permite organizar, por exemplo, um sujest para a cadeia de pesquisa, para que nem sempre que você pressione uma tecla, envie algo ao servidor, mas aguarde até que o usuário termine de digitar.

Dois ganchos semelhantes. useWindowResize retorna valores atuais atuais para tamanhos de janela. O próximo gancho para a posição de rolagem é useWindowScroll. Eu os uso para recontar alguns pop-ups ou janelas modais, se houver algo complicado que simplesmente não possa ser feito com CSS.

E um gancho tão pequeno para implementar teclas de atalho, que o componente, quando está presente na página, é inscrito em alguma tecla de atalho. Quando ele morre, ocorre um cancelamento automático da assinatura.

Para que servem esses ganchos personalizados? Que podemos enfiar uma inscrição dentro do gancho e não precisamos pensar em cancelar manualmente a inscrição em algum lugar do componente em que esse gancho é usado.

Há não muito tempo atrás, eles me deram um link para a biblioteca de uso reativo, e aconteceu que a maioria desses ganchos personalizados já estava implementada lá. E eu escrevi uma bicicleta. Isso às vezes é útil, mas no futuro, provavelmente, eu provavelmente os jogarei fora e utilizarei o react-use. E eu recomendo que você veja também se pretende usar ganchos.



Na verdade, o principal objetivo do relatório é mostrar como escrever incorretamente, quais problemas podem ser e como evitá-los. A primeira coisa, provavelmente o que qualquer pessoa que esteja estudando esses ganchos e tentando escrever algo, deve usar useEffect incorretamente. Aqui está o código semelhante ao qual 100% todos escreviam se tentassem usar ganchos. Isso se deve ao fato de que useEffect é inicialmente percebido mentalmente, como uma alternativa ao componentDidMount. Mas, diferentemente do componentDidMount, que é chamado apenas uma vez, useEffect é chamado em cada renderização. E o erro aqui é que ela altera, digamos, a variável de dados e, ao mesmo tempo, alterá-la leva a um representante de componente, como resultado, o efeito será solicitado novamente. Assim, obtemos uma série interminável de solicitações AJAX para o servidor, e o próprio componente atualiza constantemente, atualizações, atualizações.



Consertá-lo é muito simples. Você precisa adicionar aqui uma matriz vazia dessas dependências das quais depende e alterações nas quais reiniciará o efeito. Se tivermos uma lista vazia de dependências especificadas aqui, o efeito, portanto, não será reiniciado. Isso não é algum tipo de hack, é um recurso básico do uso de useEffect.



Digamos que consertamos. Agora um pouco complicado. Temos um componente que renderiza algo que precisa ser retirado do servidor para algum tipo de ID. Nesse caso, em princípio, tudo funciona bem até alterarmos o entityId no pai, talvez isso não seja relevante para o seu componente.



Mas, provavelmente, se ele mudar ou houver necessidade de alterá-lo, e você tiver um componente antigo em sua página e constatar que não está atualizando, é melhor adicionar o ID da entidade aqui, como uma dependência, causando a atualização, atualizando os dados.



Um exemplo mais complexo com useCallback. Aqui, à primeira vista, está tudo bem. Temos uma certa página que possui algum tipo de cronômetro de contagem regressiva ou, inversamente, um cronômetro que simplesmente funciona. E, por exemplo, uma lista de hosts e, em cima, são filtros que permitem filtrar essa lista de hosts. Bem, a manutenção foi adicionada aqui apenas para ilustrar um valor que muda frequentemente que se traduz em um renderizador.

, , maintenance , , , onChange. onChange, . , HostFilters - , , dropdown, . , . , .



onChange useCallback. , .

, . , , . Facebook, React. , , , , '. , , confusing .



? — , - , , , , , . .

, , , , , , . , Garbage Collector , . , , , , . , , , reducer, , . , .

, , . - , , setValue - , , setState . - useEffect.

useEffect - , - , , , useEffect. useEffect , . , , Backbone, : , , , - . , , - , . - . , , , , - . , , , , , , . .

, , . , , . , . , . , , , dropdown . , . dropdown pop-up, useWindowScroll, useWindowResize , . , , — , .

, , . , , , , , . , , , , , .



, «», . , , TypeScript . . , reducer Redux , action. , action , action. , , , .

. , action. , , IncrementA 0, 1, 2, . . , , , , . action action, - . UnionType “Action”, , , action. .

— . , initialState, . , - . TypeScript. . , typeState , initialState.



reducer. State, Action, : switch action.type. TypeScript UnionType: case, - , type. action .

, : , , . .



? , . . , reducer. , action creator , , dispatch.



extension Dev Tools. . .

, , . , , . useDebugValue , - Dev Tool. useConstants, - , loaded, , , .



— . , . , . , , , . , , — - , — .

. Facebook ESLint, . , , . , dependencies . , , , .

, , , - , . , , , . . , - - .

— , , - . , , . , , - . , . . :

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


All Articles