Você Talvez Não Precise de um Efeito
Os Efeitos são uma escotilha de escape do paradigma do React. Eles permitem que você "saia" do React e sincronize seus componentes com algum sistema externo, como um widget não-React, rede ou o DOM do navegador. Se não houver um sistema externo envolvido (por exemplo, se você quiser atualizar o estado de um componente quando algumas props ou o estado mudarem), você não deveria precisar de um Efeito. Remover Efeitos desnecessários tornará seu código mais fácil de seguir, mais rápido para executar e menos propenso a erros.
Você aprenderá
- Por que e como remover Efeitos desnecessários dos seus componentes
- Como armazenar em cache cálculos caros sem Efeitos
- Como redefinir e ajustar o estado do componente sem Efeitos
- Como compartilhar lógica entre manipuladores de eventos
- Qual lógica deve ser movida para os manipuladores de eventos
- Como notificar componentes pai sobre mudanças
Como remover Efeitos desnecessários
Existem dois casos comuns em que você não precisa de Efeitos:
- Você não precisa de Efeitos para transformar dados para renderização.Por exemplo, digamos que você queira filtrar uma lista antes de exibi-la. Você pode sentir-se tentado a escrever um Efeito que atualiza uma variável de estado quando a lista muda. No entanto, isso é ineficiente. Quando você atualiza o estado, o React primeiro chamará suas funções de componente para calcular o que deve estar na tela. Em seguida, o React irá"consolidar"essas mudanças no DOM, atualizando a tela. Então o React executará seus Efeitos. Se o seu Efeitotambématualizar imediatamente o estado, isso reinicia todo o processo do zero! Para evitar as passagens de renderização desnecessárias, transforme todos os dados no nível superior dos seus componentes. Esse código será reexecutado automaticamente sempre que suas props ou estado mudarem.
- Você não precisa de Efeitos para lidar com eventos do usuário.Por exemplo, digamos que você queira enviar uma requisição POST para
/api/buye mostrar uma notificação quando o usuário comprar um produto. No manipulador de eventos de clique do botão Comprar, você sabe exatamente o que aconteceu. No momento em que um Efeito é executado, você não sabeo queo usuário fez (por exemplo, qual botão foi clicado). É por isso que você geralmente lidará com eventos do usuário nos manipuladores de eventos correspondentes.
Vocêprecisade Efeitos parasincronizarcom sistemas externos. Por exemplo, você pode escrever um Efeito que mantém um widget jQuery sincronizado com o estado do React. Você também pode buscar dados com Efeitos: por exemplo, você pode sincronizar os resultados da pesquisa com a consulta de pesquisa atual. Lembre-se de que osframeworksmodernos fornecem mecanismos de busca de dados integrados mais eficientes do que escrever Efeitos diretamente em seus componentes.
Para ajudá-lo a ganhar a intuição correta, vamos ver alguns exemplos concretos comuns!
Atualizando o estado com base em props ou estado
Suponha que você tenha um componente com duas variáveis de estado:firstName e lastName. Você quer calcular umfullNamea partir delas concatenando-as. Além disso, você gostaria que ofullNamefosse atualizado sempre quefirstNameoulastNamemudassem. Seu primeiro instinto pode ser adicionar uma variável de estadofullNamee atualizá-la em um Efeito:
Isso é mais complicado do que o necessário. Também é ineficiente: ele faz uma passagem de renderização completa com um valor desatualizado parafullName, e então imediatamente renderiza novamente com o valor atualizado. Remova a variável de estado e o Efeito:
Quando algo pode ser calculado a partir das props ou do estado existentes,não o coloque no estado.Em vez disso, calcule-o durante a renderização.Isso torna seu código mais rápido (você evita as atualizações extras em "cascata"), mais simples (você remove algum código) e menos propenso a erros (você evita bugs causados por diferentes variáveis de estado ficarem dessincronizadas entre si). Se essa abordagem parece nova para você,Pensando em Reactexplica o que deve ir para o estado.
Armazenando em cache cálculos caros
Este componente calculavisibleTodospegando ostodosque recebe por props e filtrando-os de acordo com a propfilter. Você pode sentir a tentação de armazenar o resultado no estado e atualizá-lo a partir de um Efeito:
Como no exemplo anterior, isso é desnecessário e ineficiente. Primeiro, remova o estado e o Effect:
Normalmente, esse código está bom! Mas talvezgetFilteredTodos()seja lento ou você tenha muitostodos. Nesse caso, você não quer recalculargetFilteredTodos()se alguma variável de estado não relacionada, comonewTodo, tiver mudado.
Você pode armazenar em cache (ou"memorizar") um cálculo caro envolvendo-o em um HookuseMemo:
Observação
React Compilerpode memorizar automaticamente cálculos caros para você, eliminando a necessidade deuseMemomanual em muitos casos.
Ou, escrito em uma única linha:
Isso diz ao React que você não quer que a função interna seja executada novamente, a menos quetodosoufiltertenham mudado.O React lembrará do valor de retorno degetFilteredTodos()durante a renderização inicial. Durante as próximas renderizações, ele verificará setodosoufiltersão diferentes. Se forem os mesmos da última vez,useMemoretornará o último resultado que armazenou. Mas se forem diferentes, o React chamará a função interna novamente (e armazenará seu resultado).
A função que você envolve emuseMemoé executada durante a renderização, então isso só funciona paracálculos puros.
Reiniciando todo o estado quando uma prop muda
Este componenteProfilePagerecebe uma propuserId. A página contém uma entrada de comentário, e você usa uma variável de estadocommentpara armazenar seu valor. Um dia, você nota um problema: quando navega de um perfil para outro, o estadocommentnão é reiniciado. Como resultado, é fácil postar acidentalmente um comentário no perfil de um usuário errado. Para corrigir o problema, você quer limpar a variável de estadocommentsempre que ouserIdmudar:
Isso é ineficiente porqueProfilePagee seus filhos primeiro renderizarão com o valor desatualizado e depois renderizarão novamente. Também é complicado porque você precisaria fazer isso emtodosos componentes que têm algum estado dentro deProfilePage. Por exemplo, se a interface de comentários estiver aninhada, você também precisaria limpar o estado de comentários aninhados.
Em vez disso, você pode dizer ao React que o perfil de cada usuário é conceitualmente um perfildiferente, dando a ele uma chave explícita. Divida seu componente em dois e passe um atributokeydo componente externo para o interno:
Normalmente, o React preserva o estado quando o mesmo componente é renderizado no mesmo local.Ao passaruserIdcomo umakeypara o componenteProfile, você está pedindo ao React para tratar dois componentesProfilecom diferentesuserIdcomo dois componentes diferentes que não devem compartilhar nenhum estado.Sempre que a chave (que você definiu comouserId) mudar, o React recriará o DOM eredefinirá o estadodo componenteProfilee de todos os seus filhos. Agora, o campocommentserá limpo automaticamente ao navegar entre perfis.
Observe que, neste exemplo, apenas o componente externoProfilePageé exportado e visível para outros arquivos no projeto. Os componentes que renderizamProfilePagenão precisam passar a chave para ele: eles passamuserIdcomo uma prop regular. O fato deProfilePagepassá-lo como umakeypara o componente internoProfileé um detalhe de implementação.
Ajustando parte do estado quando uma prop muda
Às vezes, você pode querer redefinir ou ajustar uma parte do estado quando uma prop muda, mas não todo ele.
Este componenteListrecebe uma lista deitemscomo prop e mantém o item selecionado na variável de estadoselection. Você deseja redefinir aselectionparanullsempre que a propitemsreceber um array diferente:
Isso também não é ideal. Toda vez que ositemsmudarem, o componenteListe seus componentes filhos renderizarão primeiro com um valor deselectiondesatualizado. Em seguida, o React atualizará o DOM e executará os Effects. Finalmente, a chamadasetSelection(null)causará outra nova renderização do componenteListe seus componentes filhos, reiniciando todo esse processo novamente.
Comece excluindo o Effect. Em vez disso, ajuste o estado diretamente durante a renderização:
Armazenar informações de renderizações anterioresassim pode ser difícil de entender, mas é melhor do que atualizar o mesmo estado em um Effect. No exemplo acima,setSelectioné chamado diretamente durante uma renderização. O React renderizará novamente o componenteListimediatamenteapós ele sair com uma instruçãoreturn. O React ainda não renderizou os filhos deListnem atualizou o DOM, então isso permite que os filhos deListignorem a renderização do valor desatualizado deselection.
Quando você atualiza um componente durante a renderização, o React descarta o JSX retornado e tenta renderizar novamente imediatamente. Para evitar tentativas de nova renderização em cascata muito lentas, o React permite que você atualize apenas o estado domesmocomponente durante uma renderização. Se você atualizar o estado de outro componente durante uma renderização, verá um erro. Uma condição comoitems !== prevItemsé necessária para evitar loops. Você pode ajustar o estado assim, mas quaisquer outros efeitos colaterais (como alterar o DOM ou definir timeouts) devem permanecer em manipuladores de eventos ou Effects paramanter os componentes puros.
Embora esse padrão seja mais eficiente do que um Effect, a maioria dos componentes também não precisa dele.Não importa como você faça, ajustar o estado com base em props ou outro estado torna o fluxo de dados mais difícil de entender e depurar. Sempre verifique se você poderedefinir todo o estado com uma chaveoucalcular tudo durante a renderização. Por exemplo, em vez de armazenar (e redefinir) oitemselecionado, você pode armazenar oID do
Agora não há necessidade de "ajustar" o estado de forma alguma. Se o item com o ID selecionado estiver na lista, ele permanece selecionado. Se não estiver, aselectioncalculada durante a renderização seránullporque nenhum item correspondente foi encontrado. Esse comportamento é diferente, mas indiscutivelmente melhor, porque a maioria das alterações emitemspreserva a seleção.
Compartilhando lógica entre manipuladores de eventos
Digamos que você tenha uma página de produto com dois botões (Comprar e Finalizar Compra) que permitem comprar esse produto. Você deseja mostrar uma notificação sempre que o usuário colocar o produto no carrinho. ChamarshowNotification()nos manipuladores de clique de ambos os botões parece repetitivo, então você pode ficar tentado a colocar essa lógica em um Efeito:
Este Efeito é desnecessário. Ele também provavelmente causará bugs. Por exemplo, digamos que seu aplicativo "lembre" o carrinho de compras entre os recarregamentos de página. Se você adicionar um produto ao carrinho uma vez e atualizar a página, a notificação aparecerá novamente. Ela continuará aparecendo toda vez que você recarregar a página desse produto. Isso ocorre porqueproduct.isInCartjá serátrueno carregamento da página, então o Efeito acima chamaráshowNotification().
Quando você não tem certeza se algum código deve estar em um Efeito ou em um manipulador de eventos, pergunte a si mesmopor queesse código precisa ser executado. Use Efeitos apenas para códigos que devem ser executadosporqueo componente foi exibido ao usuário.Neste exemplo, a notificação deve aparecer porque o usuáriopressionou o botão, não porque a página foi exibida! Exclua o Efeito e coloque a lógica compartilhada em uma função chamada por ambos os manipuladores de eventos:
Isso remove o Efeito desnecessário e corrige o bug.
Enviando uma requisição POST
Este componenteFormenvia dois tipos de requisições POST. Ele envia um evento de análise quando é montado. Quando você preenche o formulário e clica no botão Enviar, ele enviará uma requisição POST para o endpoint/api/register:
Vamos aplicar os mesmos critérios do exemplo anterior.
A requisição POST de análise deve permanecer em um Efeito. Isso ocorre porque omotivopara enviar o evento de análise é que o formulário foi exibido. (Ele dispararia duas vezes em desenvolvimento, masveja aquipara saber como lidar com isso.)
No entanto, a requisição POST para/api/registernão é causada pelo formulário serexibido. Você só deseja enviar a requisição em um momento específico: quando o usuário pressiona o botão. Ela só deve acontecernaquela interação específica. Exclua o segundo Efeito e mova essa requisição POST para o manipulador de eventos:
Ao decidir se coloca alguma lógica em um manipulador de eventos ou em um Efeito, a principal pergunta que você precisa responder éque tipo de lógicaela é do ponto de vista do usuário. Se essa lógica for causada por uma interação específica, mantenha-a no manipulador de eventos. Se for causada pelo usuáriovero componente na tela, mantenha-a no Efeito.
Cadeias de cálculos
Às vezes você pode se sentir tentado a encadear Efeitos que ajustam um pedaço do estado com base em outro estado:
Há dois problemas com este código.
O primeiro problema é que ele é muito ineficiente: o componente (e seus filhos) precisam ser renderizados novamente entre cada chamadasetna cadeia. No exemplo acima, no pior caso (setCard→ renderização →setGoldCardCount→ renderização →setRound→ renderização →setIsGameOver→ renderização) há três renderizações desnecessárias da árvore abaixo.
O segundo problema é que, mesmo que não fosse lento, conforme seu código evolui, você encontrará casos em que a "cadeia" que você escreveu não se adequa aos novos requisitos. Imagine que você está adicionando uma forma de percorrer o histórico dos movimentos do jogo. Você faria isso atualizando cada variável de estado para um valor do passado. No entanto, definir o estadocardpara um valor do passado acionaria novamente a cadeia de Effects e alteraria os dados que você está mostrando. Esse tipo de código costuma ser rígido e frágil.
Neste caso, é melhor calcular o que você puder durante a renderização e ajustar o estado no manipulador de eventos:
Isso é muito mais eficiente. Além disso, se você implementar uma forma de visualizar o histórico do jogo, agora poderá definir cada variável de estado para um movimento do passado sem acionar a cadeia de Effects que ajusta todos os outros valores. Se precisar reutilizar a lógica entre vários manipuladores de eventos, você podeextrair uma funçãoe chamá-la desses manipuladores.
Lembre-se de que, dentro dos manipuladores de eventos, oestado se comporta como um instantâneo.Por exemplo, mesmo depois de chamarsetRound(round + 1), a variávelroundrefletirá o valor no momento em que o usuário clicou no botão. Se você precisar usar o próximo valor para cálculos, defina-o manualmente comoconst nextRound = round + 1.
Em alguns casos, vocênão podecalcular o próximo estado diretamente no manipulador de eventos. Por exemplo, imagine um formulário com várias listas suspensas onde as opções da próxima lista dependem do valor selecionado da lista anterior. Então, uma cadeia de Effects é apropriada porque você está sincronizando com a rede.
Inicializando a aplicação
Alguma lógica deve ser executada apenas uma vez quando o aplicativo carrega.
Você pode ficar tentado a colocá-la em um Efeito no componente de nível superior:
No entanto, você descobrirá rapidamente que eleé executado duas vezes no desenvolvimento.Isso pode causar problemas—por exemplo, talvez invalide o token de autenticação porque a função não foi projetada para ser chamada duas vezes. Em geral, seus componentes devem ser resilientes a serem remontados. Isso inclui seu componente de nível superiorApp.
Embora na prática ele possa nunca ser remontado em produção, seguir as mesmas restrições em todos os componentes facilita mover e reutilizar o código. Se alguma lógica deve ser executadauma vez por carregamento do aplicativoem vez deuma vez por montagem do componente, adicione uma variável de nível superior para rastrear se ela já foi executada:
Você também pode executá-la durante a inicialização do módulo e antes do aplicativo renderizar:
O código no nível superior é executado uma vez quando seu componente é importado—mesmo que ele não acabe sendo renderizado. Para evitar lentidão ou comportamento surpreendente ao importar componentes arbitrários, não use demais esse padrão. Mantenha a lógica de inicialização do aplicativo em módulos de componentes raiz comoApp.jsou no ponto de entrada do seu aplicativo.
Notificando componentes pai sobre mudanças de estado
Digamos que você está escrevendo um componenteTogglecom um estado internoisOnque pode sertrueoufalse. Existem algumas maneiras diferentes de alterná-lo (clicando ou arrastando). Você quer notificar o componente pai sempre que o estado interno doTogglemudar, então você expõe um eventoonChangee o chama a partir de um Efeito:
Como antes, isso não é ideal. OToggleatualiza seu estado primeiro, e o React atualiza a tela. Em seguida, o React executa o Effect, que chama a funçãoonChangepassada de um componente pai. Agora o componente pai atualizará seu próprio estado, iniciando outra passagem de renderização. Seria melhor fazer tudo em uma única passagem.
Exclua o Effect e, em vez disso, atualize o estado deambosos componentes dentro do mesmo manipulador de eventos:
Com essa abordagem, tanto o componenteTogglequanto seu componente pai atualizam seu estado durante o evento. O Reactagrupa atualizaçõesde diferentes componentes juntas, então haverá apenas uma passagem de renderização.
Você também pode ser capaz de remover o estado completamente e, em vez disso, receberisOndo componente pai:
“Elevar o estado”permite que o componente pai controle totalmente oTogglealternando o próprio estado do pai. Isso significa que o componente pai terá que conter mais lógica, mas haverá menos estado no geral para se preocupar. Sempre que você tentar manter duas variáveis de estado diferentes sincronizadas, tente elevar o estado em vez disso!
Passando dados para o pai
Este componenteChildbusca alguns dados e depois os passa para o componenteParentem um Effect:
No React, os dados fluem dos componentes pai para seus filhos. Quando você vê algo errado na tela, pode rastrear de onde a informação vem subindo a cadeia de componentes até encontrar qual componente passa a prop errada ou tem o estado errado. Quando componentes filhos atualizam o estado de seus componentes pai em Effects, o fluxo de dados se torna muito difícil de rastrear. Como tanto o filho quanto o pai precisam dos mesmos dados, deixe o componente pai buscar esses dados epasse-os para baixopara o filho em vez disso:
Isso é mais simples e mantém o fluxo de dados previsível: os dados fluem de cima para baixo, do pai para o filho.
Inscrevendo-se em uma store externa
Às vezes, seus componentes podem precisar se inscrever em alguns dados fora do estado do React. Esses dados podem vir de uma biblioteca de terceiros ou de uma API de navegador integrada. Como esses dados podem mudar sem o conhecimento do React, você precisa inscrever manualmente seus componentes neles. Isso geralmente é feito com um Effect, por exemplo:
Aqui, o componente se inscreve em uma store de dados externa (neste caso, a API do navegadornavigator.onLine). Como essa API não existe no servidor (portanto, não pode ser usada para o HTML inicial), inicialmente o estado é definido comotrue. Sempre que o valor dessa store de dados muda no navegador, o componente atualiza seu estado.
Embora seja comum usar Effects para isso, o React tem um Hook específico para se inscrever em uma store externa que é preferido em vez disso. Exclua o Effect e substitua-o por uma chamada auseSyncExternalStore:
Essa abordagem é menos propensa a erros do que sincronizar manualmente dados mutáveis com o estado do React usando um Effect. Normalmente, você escreverá um Hook personalizado comouseOnlineStatus()acima para não precisar repetir esse código nos componentes individuais.Leia mais sobre como se inscrever em stores externas a partir de componentes React.
Buscando dados
Muitos aplicativos usam Effects para iniciar a busca de dados. É bastante comum escrever um Effect de busca de dados assim:
Vocênão precisamover esta busca para um manipulador de eventos.
Isso pode parecer uma contradição com os exemplos anteriores, onde você precisava colocar a lógica nos manipuladores de eventos! No entanto, considere que não éo evento de digitaçãoque é o principal motivo para buscar. As entradas de pesquisa são frequentemente pré-preenchidas a partir da URL, e o usuário pode navegar para Trás e para Frente sem tocar na entrada.
Não importa de ondepage e queryvêm. Enquanto este componente estiver visível, você deseja manter osresultssincronizadoscom os dados da rede para apage e queryatuais. É por isso que é um Efeito.
No entanto, o código acima tem um bug. Imagine que você digite"hello"rapidamente. Então aquerymudará de"h", para"he","hel","hell", e"hello". Isso iniciará buscas separadas, mas não há garantia sobre a ordem em que as respostas chegarão. Por exemplo, a resposta para"hell"pode chegardepoisda resposta para"hello". Como ela chamarásetResults()por último, você estará exibindo os resultados de pesquisa errados. Isso é chamado de"condição de corrida": duas requisições diferentes "correram" uma contra a outra e chegaram em uma ordem diferente da esperada.
Para corrigir a condição de corrida, você precisaadicionar uma função de limpezapara ignorar respostas desatualizadas:
Isso garante que, quando seu Efeito buscar dados, todas as respostas, exceto a última solicitada, serão ignoradas.
Lidar com condições de corrida não é a única dificuldade na implementação da busca de dados. Você também pode querer pensar em armazenar em cache as respostas (para que o usuário possa clicar em Voltar e ver a tela anterior instantaneamente), como buscar dados no servidor (para que o HTML renderizado inicialmente no servidor contenha o conteúdo buscado em vez de um spinner) e como evitar cachoeiras de rede (para que um componente filho possa buscar dados sem esperar por todos os pais).
Essas questões se aplicam a qualquer biblioteca de UI, não apenas ao React. Resolvê-las não é trivial, por isso osframeworksmodernos fornecem mecanismos de busca de dados integrados mais eficientes do que buscar dados em Efeitos.
Se você não usa um framework (e não quer construir o seu próprio), mas gostaria de tornar a busca de dados a partir de Efeitos mais ergonômica, considere extrair sua lógica de busca em um Hook personalizado, como neste exemplo:
Você provavelmente também vai querer adicionar alguma lógica para tratamento de erros e para rastrear se o conteúdo está carregando. Você pode construir um Hook como este por conta própria ou usar uma das muitas soluções já disponíveis no ecossistema React.Embora isso sozinho não seja tão eficiente quanto usar o mecanismo de busca de dados integrado de um framework, mover a lógica de busca de dados para um Hook personalizado facilitará a adoção de uma estratégia de busca de dados eficiente posteriormente.
Em geral, sempre que você tiver que recorrer a escrever Efeitos, fique atento para quando puder extrair uma funcionalidade em um Hook personalizado com uma API mais declarativa e específica, comouseDataacima. Quanto menos chamadas brutas deuseEffectvocê tiver em seus componentes, mais fácil será manter sua aplicação.
Recapitulação
- Se você pode calcular algo durante a renderização, não precisa de um Efeito.
- Para armazenar em cache cálculos caros, adicione
useMemoem vez deuseEffect. - Para redefinir o estado de uma árvore de componentes inteira, passe uma
keydiferente para ela. - Para redefinir um pedaço específico de estado em resposta a uma mudança de prop, defina-o durante a renderização.
- Código que é executado porque um componente foiexibidodeve estar em Efeitos, o resto deve estar em eventos.
- Se você precisa atualizar o estado de vários componentes, é melhor fazer isso durante um único evento.
- Sempre que você tentar sincronizar variáveis de estado em componentes diferentes, considere elevar o estado.
- Você pode buscar dados com Efeitos, mas precisa implementar limpeza para evitar condições de corrida.
Try out some challenges
Challenge 1 of 4:Transform data without Effects #
The TodoList below displays a list of todos. When the “Show only active todos” checkbox is ticked, completed todos are not displayed in the list. Regardless of which todos are visible, the footer displays the count of todos that are not yet completed.
Simplify this component by removing all the unnecessary state and Effects.
