React: Guia Visual para o Modelo Mental do React, Parte 2 - useState, useEffect e ciclos de vida
Eduardo Rabelo
Posted on September 23, 2020
Eu amo modelos mentais. Eles são cruciais para a compreensão de sistemas complexos, nos permitindo compreender e resolver problemas complexos de forma intuitiva.
Este é o segundo de uma série de três artigos sobre os modelos mentais do React. Vou mostrar os modelos mentais exatos que uso com complexos componentes React, construindo-os do zero e usando várias explicações visuais.
Recomendo que você leia a parte 1 primeiro, pois os modelos mentais neste artigo se baseiam nos que expliquei lá. Se você quiser uma atualização, aqui está o modelo mental completo para a parte 1
Mesmo que você já trabalhe com o React há anos ou esteja apenas começando, ter um modelo mental útil é, na minha opinião, é a maneira mais rápida de se sentir confiante trabalhando com ele.
Você vai aprender:
- O hook useState : Como ele magicamente funciona e como entendê-lo intuitivamente.
- O ciclo de vida do componente: montagem, renderização, desmontagem : a fonte de muitos bugs é a falta de um bom modelo mental em torno deles.
- O hook useEffect : como esse gancho poderoso realmente funciona?
Vamos começar!
O Que São Modelos Mentais E Por Que São Importantes?
Um modelo mental é um processo de pensamento ou imagem mental que nos ajuda a compreender sistemas complexos e a resolver problemas difíceis intuitivamente, guiando-nos na direção certa. Você usa modelos mentais todos os dias; pense em como você imagina que a Internet, os carros ou o sistema imunológico funcionem. Você tem um modelo mental para cada sistema complexo com o qual interage.
O Modelo Mental Para React Até Agora
Aqui está uma visão geral rápida do modelo mental do React que expliquei na parte 1, ou você pode encontrar a versão completa da parte 1 aqui .
Um componente React é como uma função, ele recebe props
das quais são os argumentos de uma função e será executado novamente sempre que essas props mudarem. Eu imagino um componente como uma caixa que vive dentro de outra caixa.
Cada caixa pode ter muitos filhos, mas apenas um pai, e além de receber props
de seu pai, ela tem uma variável interna especial chamada state
, que também a re-executa (re-renderizada) quando muda.
Quando adereços ou estado muda, o componente é renderizado novamente
O hook useState: Estado Em Uma Garrafa
Mostrei como o estado funciona na parte 1 e como ele é uma propriedade especial dentro de uma caixa. Ao contrário das variáveis ou funções que são declaradas novamente em cada renderização, os valores que saem do useState
sempre são consistentes entre as renderizações. Eles são inicializados no mount
com um valor padrão e só podem ser alterados por um evento setState
.
Mas como o React pode evita que o state
perca seu valor em cada renderização?
A resposta é: o escopo.
Expliquei o modelo mental para closures e escopo no passo 1 . Em suma, uma closure é como uma caixa semipermeável, permitindo a entrada de informações de fora, mas nunca vazando nada.
Com useState
, React define seu valor para a closure mais externa, que é o aplicativo React que contém todos os seus componentes. Em outras palavras, sempre que você usa o useState
, React retorna um valor que é armazenado fora do seu componente e, portanto, não muda em cada renderização.
O React consegue fazer isso acompanhando cada componente e a ordem em que cada hook é declarado. Essa é a razão pela qual você não pode ter um React Hook dentro de uma condicional. Se useState, useEffect ou qualquer outro hook for criado condicionalmente , o React não poderá controlá-lo adequadamente.
Isso é melhor explicado visualmente:
Sempre que um componente é renderizado novamente, useState
pede o estado do componente atual, o React verifica uma lista contendo todos os estados de cada componente e retorna o valor correspondente. Esta lista é armazenada fora do componente porque em cada uma das variáveis de renderização e funções são criadas e destruídas em cada renderização.
Embora esta seja uma visão técnica de como o estado funciona, ao compreendê-la, posso transformar parte da magia do React em algo que posso visualizar. Para meu modelo mental, eu costumo simplificar as coisas em uma ideia mais simples.
Meu modelo mental ao trabalhar com useState
é este: uma vez que o estado não é afetado pelo que acontece com a caixa, imagino-o como um valor constante dentro dela. Eu sei que não importa o que aconteça, o state
permanecerá consistente durante toda a vida útil do meu componente.
O estado permanece constante, embora o componente possa mudar
Como O Estado Muda?
Depois de entender como o estado é preservado, é importante entender como ele muda.
Você pode saber que as atualizações de estado são async
, mas o que isso significa? Como isso afeta nosso trabalho diário?
Uma explicação simplificada de sync
e async
é:
- Código síncrono: que bloqueia a thread do JavaScript, onde seus aplicativos são executados, impedidos de fazer qualquer outro trabalho. Apenas um trecho de código pode ser executado por vez no segmento.
- Código assíncrono: que não bloqueia o thread porque ele é movido para uma fila e executado sempre que houver tempo disponível.
Usamos o estado como uma variável, mas a atualização é async
. Isso torna mais fácil cair na armadilha de pensar que um setState
mudará seu valor imediatamente como faria uma variável, o que leva a bugs e frustração, por exemplo:
const Component = () => {
const [searchValue, setSearchValue] = useState('');
// procura alguma coisa quando o usuário escreve no inout
const handleInput = e => {
// salva o valor no estado e em seguida, o usa para buscar novos dados ❌
setSearchValue(e.target.value);
fetchSearch(searchValue).then(results => {
// faz algo
});
};
};
Este código está cheio de erros. Imagine uma pessoa digitar Bye. O código procurará por By vez de Bye porque cada nova digitação aciona um novo setSearchValue
e fetchSearch
, mas como as atualizações de estado são async
, vamos buscar com um valor desatualizado para searchValue
. Se uma pessoa digitar rápido o suficiente e tiver outro código JavaScript em execução, podemos até pesquisar apenas por B pois o JavaScript ainda não teve tempo de executar o código que está na fila.
Resumindo, não espere state
ser atualizado imediatamente. Isso corrige o bug:
const Component = () => {
const [searchValue, setSearchValue] = useState('');
const handleInput = e => {
// salvamos o valor pesquisado em uma variável antes de usar ✅
const search = e.target.value;
setSearchValue(search);
fetchSearch(search).then(results => {
// do something
});
};
};
Um dos motivos pelos quais as atualizações de estado são async
é para otimizáção. Se um aplicativo tiver centenas de estados diferentes que desejam atualizar de uma só vez, o React tentará agrupar o maior número possível em uma única operação async
, ao invés de executar muitos eventos sync
. As operações assíncronas, em geral, também têm melhor desempenho.
Outro motivo é a consistência. Se um estado for atualizado muitas vezes em rápida sucessão, o React usará apenas o valor mais recente por uma questão de consistência. Isso seria difícil de fazer se as atualizações fossem sync
e executadas imediatamente.
Em meu modelo mental, vejo os valores de estados individuais sendo confiáveis, mas lentos. Sempre que atualizo um, sei que pode demorar um pouco para que ele mude.
Mas o que acontece com o estado e o próprio componente, quando ele é montado e desmontado?
Ciclo De Vida De Um Componente: Modelos Mentais Para Montagem, Renderização E Desmontagem
Anteriormente, falávamos muito sobre métodos de ciclo de vida, quando apenas os componentes de classe tinham acesso a state
e controle do que estava acontecendo com um componente durante sua vida. Mas desde que os Hooks surgiram, nos permitindo o mesmo tipo de poder em componentes funcionais, a ideia se tornou menos relevante.
O interessante é que cada componente ainda tenha um ciclo de vida: é montado, renderizado e desmontado, e cada etapa deve ser levada em consideração para um modelo mental totalmente funcional em torno dos componentes React.
Portanto, vamos passar por cada fase e construir um modelo mental para isso, prometo que tornará sua compreensão de um componente muito melhor.
Montagem: Criando Componentes
Quando o React cria ou renderiza um componente pela primeira vez, é a fase de mounting
dele. O que significa que será adicionado ao DOM e o React começará a controlá-lo.
Gosto de imaginar mounting
como uma nova caixa sendo e/ou adicionada dentro de seu pai.
A montagem acontece sempre que um componente que não foi renderizado ainda, e o seu componente pai decide renderizá-lo pela primeira vez. Em outras palavras, mounting
é quando um componente que está "nascendo".
Um componente pode ser criado e destruído muitas vezes e, a cada vez que for criado, ele será montado novamente.
const Component = () => {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(!show)}>Show Menu</button>
// Montando com `show = true` e desmontado com `show = fase`
{show && <MenuDropdown />}
</div>
);
};
O React renderiza componentes tão rápido que pode parecer que ele está os escondendo, mas na realidade, ele os cria e exclui muito rapidamente. No exemplo acima, o componente <MenuDropdown />
será adicionado e removido do DOM sempre que o botão for clicado.
Observe como o pai do componente é quem decide quando montar e desmontar <MenuDropdown />
. Isso é a hierarquia de componentes. Se MenuDropdown
tiver componentes filhos, eles serão montados ou desmontados também. O próprio componente nunca sabe quando será montado ou desmontado.
Uma vez que um componente é mounted
(montado), ele fará algumas coisas:
- Inicializar
useState
com os valores padrão: isso só acontece na montagem. - Executa a lógica do componente.
- Faz uma renderização inicial, adicionando os elementos ao DOM.
- Executa o hook
useEffect
.
Observe que o useEffect
é executado após a renderização inicial. É quando você deseja executar o código, como a criação de ouvintes de eventos (event subscribers), a execução de lógica pesada ou a obtenção de dados (data fetching). Mais sobre isso na seção useEffect abaixo.
Meu modelo mental para mounting
é o seguinte: sempre que uma caixa pai decide que um filho deve ser criado, ele a monta, então o componente fará três coisas: atribuir valores padrão a useState
, executar sua lógica, renderizar e executar o hook useEffect
.
A fase de mount
é muito semelhante a um normal re-render
, com a diferença de inicializar useState
com valores padrão e os elementos sendo adicionados ao DOM pela primeira vez. Depois que o componente realiza o mount
, ele permanece no DOM e é atualizado posteriormente.
Uma vez que um componente é montado, ele continuará funcionando até que seja desmontado, fazendo qualquer quantidade de renderizações entre eles.
Renderizando: Atualizando O Que O Usuário Vê
Expliquei o modelo mental de renderização na parte 1, mas vamos revisá-lo brevemente, pois é uma fase importante.
Depois que um componente é montado, quaisquer alterações em props
ou state
farão com que seja renderizado novamente, re-executando todo o código dentro dele, incluindo seus componentes filhos. Depois de cada render
o hook useEffect
é avaliado novamente.
Imagino um componente como uma caixa e sua capacidade de renderizar novamente o torna uma caixa reutilizável. Cada render recicla a caixa, o que poderia gerar informações diferentes, mantendo o mesmo estado e código por baixo.
Uma vez que o pai de um componente decide parar de renderizar um filho - por causa de uma condição, mudanças nos dados ou qualquer outro motivo - o componente precisará ser desmontado.
Desmontagem: Excluindo Componentes
Quando a fase de unmounted
em um componente é ativado, o React o remove do DOM e para de controlá-lo. O componente é excluído, incluindo qualquer state
que ele tinha
Como explicado na fase de mounting
, um componente é ao mesmo tempo mounted
e unmounted
por seu pai, e se o componente, por sua vez, tiver filhos, eles também terão a fase de unmount
, e o ciclo se repete até que o último filho seja alcançado.
Em meu modelo mental, vejo isso como uma caixa-pai destruindo suas caixas-filhos. Se você jogar um contêiner no lixo, tudo dentro dele também irá para o lixo, isso inclui outras caixas (componentes), estado, variáveis, tudo.
Mas um componente pode criar código fora de si mesmo. O que acontece com qualquer assinatura (subscription), soquete da web (websockets) ou ouvinte de evento (event listeners) criado por um componente que será desmontado?
A resposta é nada. Essas funções são executadas fora do componente e não serão afetadas por sua exclusão. É por isso que é importante que o componente seja limpo antes de desmontar.
Cada função consome recursos. Deixar de limpá-los pode levar a bugs desagradáveis, desempenho degradado e até riscos de segurança.
Eu penso nessas funções como engrenagens girando fora da minha caixa. Eles são colocados em movimento quando o componente monta e devem ser interrompidos quando desmontados.
Podemos limpar ou parar essas engrenagens por meio da função de retorno do useEffect
. Explicarei em detalhes na seção hook useEffect.
Então, vamos colocar todos os métodos de ciclo de vida em um modelo mental claro.
O Modelo Mental Completo Do Ciclo De Vida Do Componente
Para resumir o que vimos até agora: um componente é apenas uma função, props são os argumentos da função e o estado é um valor especial que o React garante manter consistente entre as renderizações. Todos os componentes devem estar dentro de outros componentes e cada pai pode ter muitos filhos dentro dele.
Cada componente tem três fases em seu ciclo de vida: montagem, renderização e desmontagem.
Em meu modelo mental, um componente é uma caixa e com base em alguma lógica pode decidir criar ou excluir uma caixa-filho. Quando o cria, é um componente montado e quando o exclui, ele é desmontado.
Uma caixa montando significa que foi criada e executada. É aqui que useState
é inicializado com os valores padrão e o React o renderiza para que o usuário possa vê-lo e também começa a controlá-lo.
A fase de montagem é onde nos conectamos a serviços externos, buscamos dados ou criamos escutadores de eventos (event listeners).
Uma vez montado, sempre que as pros ou estado de uma caixa mudarem, ela será refeita, o que eu imagino como a caixa sendo reciclada e tudo, exceto o estado, é re-executado e re-calculado. O que o usuário vê pode mudar a cada nova renderização. A re-renderização é a segunda fase, que pode acontecer inúmeras vezes, sem limite.
Quando a caixa-pai de um componente decide removê-lo, seja por causa da lógica, o próprio pai foi removido ou os dados foram alterados, o componente o fará a fase de desmontagem.
Quando uma caixa desmontada é jogada fora, vai para o lixo com tudo o que contém, incluindo componentes filhos (que por sua vez têm os suas próprias fases de desmontagem). É aqui que temos a chance de limpar e excluir qualquer função externa inicializada em useEffect
.
O ciclo de montagem, renderização e desmontagem pode acontecer milhares de vezes em seu aplicativo sem que você perceba. O React é incrivelmente rápido e é por isso que é útil manter um modelo mental em mente ao lidar com componentes complexos, já que é tão difícil ver o que está acontecendo em tempo real.
Mas como tiramos proveito dessas fases em nosso código? A resposta está no poderoso hook useEffect
.
O hook useEffect: Poder ilimitado!
O hook de efeito nos permite executar efeitos colaterais em nossos componentes. Sempre que você está buscando dados, conectando-se a um serviço ou assinatura ou manipulando manualmente o DOM, está realizando um efeito colateral (também simplesmente chamado de "efeito").
Um efeito colateral no contexto de funções é qualquer coisa que torne a função imprevisível, como dados ou estado. Uma função sem efeitos colaterais será previsível e pura - você deve ter ouvido falar de pure functions
- sempre fazendo exatamente a mesma coisa, desde que as entradas permaneçam constantes.
Um hook de efeito sempre é executado após cada renderização. O motivo é que os efeitos colaterais podem conter lógica pesada ou demorar, como a obtenção de dados, portanto, em geral, é melhor executar após a renderização.
O hook recebe dois argumentos: a função a ser executada e um array com valores que serão avaliados após cada renderização, esses valores são chamados de dependências.
// Opção 1 - sem dependências
useEffect(() => {
// lógica pesada que roda depois de cada renderização
});
// Opção 2 - dependências vazias
useEffect(() => {
// cria um escutador de eventos (event listener), inscrição (subscription)
// ou busca dados uma única (fetch one-time data)
}, []);
// Opção 3 - com dependências
useEffect(() => {
// busca dados sempre que A, B ou C mudarem
}, [a, b, c]);
Dependendo do segundo argumento, você tem 3 opções com comportamentos diferentes. A lógica de cada opção é:
- Se não estiver presente, o efeito será executado após cada renderização. Esta opção não é normalmente usada, mas é útil em algumas situações, como a necessidade de fazer cálculos pesados após cada renderização.
- Com um array vazio,
[]
o efeito é executado apenas uma vez, após a montagem e na primeira renderização. Isso é ótimo para efeitos únicos, como criar um escutador de eventos (event listener). - Um array com valores
[a, b, c]
faz com que o efeito avalie as dependências, sempre que uma dependência mudar o efeito será executado. Isso é útil para executar efeitos quando props ou o estado mudam, como buscar novos dados.
O array de dependência da ao useEffect
sua mágica e é importante usá-la corretamente. Você deve incluir todas as variáveis usadas em useEffect
, caso contrário, o efeito fará referência a valores obsoletos de renderizações anteriores durante a execução, causando bugs.
O plugin ESLint eslint-plugin-react-hooks
contém muitas regras específicas de Hooks úteis, incluindo uma que irá avisá-lo se você perdeu uma dependência dentro de um useEffect
.
Meu modelo mental inicial para useEffect é como ter uma mini-caixa dentro de seu componente, com três comportamentos distintos dependendo do uso do array de dependência: o efeito é executado após cada renderização se não houver dependências, apenas após a montagem se for um array vazio, ou sempre que uma dependência muda se o array tiver valores.
Há outro recurso importante do useEffect
, nos permite fazer a limpeza antes que um novo efeito seja executado ou antes que a desmontagem ocorra.
Limpeza: useEffect durante a desmontagem
Cada vez que criamos uma assinatura, escutador de eventos ou conexões abertas, devemos limpá-los quando não forem mais necessários, caso contrário, criamos um vazamento de memória e degradamos o desempenho de nosso aplicativo.
É aqui que useEffect
vem a calhar. Ao retornar uma função dele, podemos executar o código antes de aplicar o próximo efeito ou, se o efeito for executado apenas uma vez, o código será executado antes da desmontagem do componente.
// Esse efeito irá executar uma ver na montagem, criando um escutador de eventos
// Na fase de desmontagem, irá executar a função que está sendo retornada
// removendo o escutador de eventos e limpando nossa bagunça ✅
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.remoteEventListener('resize', handleResize);
}, []);
// Esse efeito irá executar sempre que o valor de `props.stream.id` mudar
useEffect(() => {
const handleStatusChange = streamData => {
setStreamData(streamData);
};
streamingApi.subscribeToId(props.stream.id, handleStatusChange);
// Cancela a inscrição do ID atual antes de executar o próximo efeito com novo ID
return () =>
streamingApi.unsubscribeToId(props.stream.id, handleStatusChange);
}, [props.stream.id]);
O modelo mental completo do React Hook useEffect
Imagino o useEffect como uma pequena caixa dentro de um componente, vivendo ao lado da lógica do componente. O código desta caixa (chamado de efeito) só é executado depois que o React renderizou o componente, e é o lugar perfeito para executar efeitos colaterais ou alguma lógica pesada.
Toda a magia do useEffect vem de seu segundo argumento, o array de dependência, e pode ter três comportamentos a partir dela:
- Sem argumento: o efeito é executado após cada renderização
- Array vazio: o efeito só é executado após a renderização inicial e a função de retorno antes da desmontagem.
- Array com valores: sempre que uma dependência muda, o efeito será executado e a função de retorno será executada antes do novo efeito.
Espero que você tenha achado meus modelos mentais úteis! Tentar explicá-los foi claramente um desafio. Se você gostou de ler, compartilhe este artigo, é tudo o que peço ❤️.
Esta foi a segunda parte de uma série de três partes, a próxima e última, abordarão conceitos de alto nível, como React context
e como pensar melhor em seu aplicativo para evitar problemas comuns de desempenho.
Estou planejando uma série de guias visuais. A melhor maneira de saber quando eles serão lançados é assinar minha newsletter. Eu só envio emails de artigos novos de alta qualidade.
Que perguntas você tem? Estou sempre disponível no Twitter!
Créditos
- A Visual Guide To React Mental Models, Part 2: UseState, UseEffect And Lifecycles, escrito originalmente por Obed Parlapiano.
Posted on September 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 23, 2020