Componentes de Estado (useEffect)

Componentes de Estado (useEffect)

Em lições passadas, você aprendeu sobre o uso dos métodos componentDidMount, componentDidUpdate e componentWillUnmount pertencentes ao componentes de classe.

Você ainda se lembra o que cada um deles faziam?

Todos eles eram métodos usados para controlar o ciclo de vida de uma aplicação em ReactJS, onde podiamos executar funções e operações enquanto nossos componentes eram montados no DOM.

Mas se tratanto de componentes de estado, como será que podemos controlar o ciclo de vida das nossas aplicações?

useEffect

O useEffect veio para substituir todos os ciclos de vida dos componentes de classe em ReactJS. (ComponentDidMount, ComponentDidUpdate e etc).

Ele nada mais é do que um hook (assim como o useState) que nos permite executar efeitos colaterais em componentes funcionais.

Quando me refiro a "efeitos colaterias" estou querendo dizer que é a partir do useEffect que nos podemos realizar a busca de dados de uma API, a assinatura de eventos ou manipulações no DOM, e tudo isso antes, durante e após a renderização do componente.

De certa forma, ele é muuuito similar aos métodos componentDidMount, componentDidUpdate e componentWillUnmount, a única diferença é que ele faz tudo isso dentro de uma única função anônima 😉

Partiu colocar a mão na massa?

Criando seu projeto de testes

Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a esta lição. No meu caso criei um novo projeto chamado de componentes-de-estado-useeffect:  

npx create-react-app componentes-de-estado-useeffect

Após isso, não se esqueça de fazer aquela limpeza do código, e também aquela organizada nas pastas do src (organização por funcionalidade) 😁  

Em seguida vamos criar o nosso primeiro componente chamado de MeuComponente.

MeuComponente > index.jsx:

const MeuComponente = () => {
 return(
 <>
 <h1>Meu Componente</h1>
 </>
 )
}

export default MeuComponente;

Não se esqueça também de inserir o seu componente dentro do App.js:

import MeuComponente from "./components/MeuComponente";

export default function App() {
 return(
 <div>
 <MeuComponente />
 </div>
 )
}

Conseguiu? Então bora aprender um pouco mais sobre esse tal de useEffect!

Importando o useEffect

É importante ressaltar que o useEffect é um hook que já vem instalado dentro da sua aplicação em ReactJS.

Para usar este tipo de hook dentro do seu componente, você vai precisar importá-lo dentro do seu componente da seguinte forma:

import React, { useEffect } from 'react';

Lembrando que você pode usar outros hooks em conjunto com o useEffect, para isso não se esqueça de importá-los dentro do seu componente, por exemplo:

import React, { useEffect, useState } from 'react';//Exemplo de importação dos hooks useEffect e useState

Feito isso, vamos importá-lo também dentro do MeuComponente e seguir a diante 🙂

Usando o useEffect

Para usar o useEffecté bem simples, basta que você o declare dentro do corpo do seu componente, onde o primeiro argumento sempre será uma função anônima (que será executada pelo ReactJS), e o segundo argumento um array (dependências do hook).

Entrando em mais detalhes, o primeiro argumento sempre será uma função de callback, onde o segundo representa um array de dependências, vamos ver como essa declaração acontece na prática:

useEffect(() => {
 //Execute algumas lógicas por aqui!
}, []);

Simples não? Agora vamos destrinchar ainda mais o funcionamento do useEffect.

MeuComponente > index.jsx:

import React, { useEffect } from 'react';

const MeuComponente = () => {

 useEffect(() => {
 //Execute algumas lógicas por aqui!
 }, []);
 
 return(
 <>
 <h1>Meu Componente</h1>
 </>
 )
}
 
export default MeuComponente;

Se você colocar a sua aplicação para rodar (npm start), verás que nada de muito estanho aconteceu, e isso é meio obvio, pois não executamos nada dentro do nosso useEffect 😝

Pois bem, como dito anteriormente o useEffect veio para substituir os métodos que controlavam o ciclo de vida das nossas aplicações.

Sempre quando um componente está para ser montado na tela, por de baixo dos panos, o ReactJS sai a procura das declarações de useEffect que podem existir dentro do corpo do seu componente.

Em um primeiro momento, o ReactJS vai executar todas as funções de callback (primeiro argumento do seu useEffect) nas quais o segundo argumento do useEffect estiver declarado como um array VAZIO! (ou seja, sem dependências informadas).

Por exemplo:

import React, { useEffect } from 'react';

const MeuComponente = () => {

 useEffect(() => {
 console.log('MeuComponente foi montado');
 }, []);

 return(
 <>
 <h1>Meu Componente</h1>
 </>
 )
}
 
export default MeuComponente;

Quando declaramos um useEffect onde não passamos nenhuma variável para dentro do array que representa o segundo argumento, significa que o useEffect será executado apenas uma vez, logo após a montagem do componente na tela do usuário.

Isso é análogo ao comportamento do método componentDidMount que vimos nos componentes de classe.

Observe que assim que você abre a sua aplicação, uma mensagem no console será disparada, dizendo "MeuComponente foi montado!".

Atenção!

Quando eu digo que o componente é "montado", estou me referindo ao momento em que o componente é renderizado pela primeira vez no DOM e se torna visível para o usuário.

E isso significa que tudo o que colocarmos ali dentro daquela função anônima, será executado logo após que o seu componente for montado no DOM do navegador do usuário.

É dentro do seu useEffect que você pode realizar algumas operações de efeitos colateriais, por exemplo:

Chamadas de API: Realizar solicitações para obter ou enviar dados para um servidor.

Atualizações de estado: Modificar o estado do componente usando useState ou outros hooks de estado relacionados.

Assinatura de eventos: Adicionar event listeners para manipular eventos do navegador ou de outros elementos do DOM.

Manipulação do DOM: Fazer alterações diretas no DOM, como adicionar ou remover elementos, alterar estilos ou classes, etc.

Limpando efeitos anteriores: Retornar uma função de limpeza para limpar efeitos anteriores, como cancelar assinaturas de eventos ou temporizadores, para evitar vazamentos de memória ou outros problemas.

Isso ajuda a garantir que o componente permaneça coeso e siga o princípio de "separação de interesses", onde a lógica relacionada ao efeito colateral é isolada do código que controla a renderização e o estado do componente.

Usando o return dentro de um useEffect

Dentro do corpo da nossa função anônima (callback) do nosso useEffect, nós podemos executar um return no final do código da seguinte forma:

useEffect(() => {
 console.log('MeuComponente foi montado');
 return () => {
 console.log('MeuComponente foi desmontado');
 }
}, []);

O return nada mais é do uma função anônima (também rs) que sempre será chamada quando o seu componente for desmontado. O que simula o método componentWillAmount que é chamado logo após o termino do componente de classe.

Ali é o local que todo desenvolver deve realizar a limpeza de efeitos anteriores, especialmente quando esses efeitos envolvem ações como assinaturas de eventos, temporizadores ou outras operações que precisam ser desfeitas quando o componente é desmontado ou quando os efeitos são reexecutados.

Quantos useEffect podem existir dentro de um componente?

Não há limites rígidos para o número de useEffect que podem existir dentro de um componente no ReactJS. Você pode ter quantos useEffect forem necessários para lidar com diferentes efeitos colaterais em seu componente.

Por exemplo:

import React, { useEffect } from 'react';

const MeuComponente = () => {

 useEffect(() => {
 console.log("useEffect um");
 }, []);

 useEffect(() => {
 console.log("useEffect dois");
 }, []);

 useEffect(() => {
 console.log("useEffect tres");
 }, []);
 
 return(
 <>
 <h1>Meu Componente</h1>
 </>
 )
}
 
export default MeuComponente;

Observe que no código acima existem diversos useEffect onde cada um deles é executado.

Usando o useEffect da maneira correta

Agora chegou o momento de analisarmos diversos exemplos da real utilização de um useEffect no seu projeto ReactJS.

Realizando chamadas em API

Anteriormente foi dito que o useEffect é usado para executar efeitos colateriais, tais como uma simples chamada em uma API quando o componente é montado.

Para testarmos isso, vamos criar um novo componente chamado de RetornaAPI, cujo o objetivo é consumir os dados de uma determinada API de modo a mostrar os resultados no HTML (JSX) do componente.

RetornaAPI > index.jsx:

import React, { useEffect } from 'react';

const RetornaAPI = () => {
 useEffect(() => {
 const fetchData = async () => {
 try {
 const response = await fetch('https://jsonplaceholder.typicode.com/users');
 const data = await response.json();
 // Apenas para ilustração, vamos pegar apenas os nomes dos usuários
 const nomesUsuarios = data.map(user => user.name);
 console.log('Nomes da API:', nomesUsuarios);
 } catch (error) {
 console.error('Erro ao buscar dados da API:', error);
 }
 };

 fetchData();
 }, []); // A dependência está vazia, então o useEffect será executado apenas uma vez
 

 return (
 <div>
 <h2>Retorna API</h2>
 </div>
 );
};

export default RetornaAPI;

O resultado pode ser visto dentro do console.log do seu navegador, observe:

Lembrando que essa duplicação refere-se ao StrictMode do index.js, onde em jornadas anteriores eu já expliquei como você lida com isso 🙃

Para saber mais sobre o funcionamento da função fetch do Javascript, clique aqui.

Perfeito, o código acima executa uma chamada em uma API externa de modo a mostrar os resultados no console da aplicação.

Só que, em uma aplicação real isso não funciona, pois a graça mesmo é fazer com o seu usuário visualize a lista de nomes não é verdade? 😆

Nesse caso, inevitavelmente vamos precisar do nosso querido useState que aprendemos na lição passada.

RetornaAPI > index.jsx:

import React, { useState, useEffect } from 'react';

const RetornaAPI = () => {
 const [nomes, setNomes] = useState([]);

 useEffect(() => {
 const fetchData = async () => {
 try {
 const response = await fetch('https://jsonplaceholder.typicode.com/users');
 const data = await response.json();
 // Apenas para ilustração, vamos pegar apenas os nomes dos usuários
 const nomesUsuarios = data.map(user => user.name);
 setNomes(nomesUsuarios);
 } catch (error) {
 console.error('Erro ao buscar dados da API:', error);
 }
 };

 fetchData();
 }, []); // A dependência está vazia, então o useEffect será executado apenas uma vez

 return (
 <div>
 <h2>Nomes da API:</h2>
 <ul>
 {nomes.map((nome, index) => (
 <li key={index}>{nome}</li>
 ))}
 </ul>
 </div>
 );
};

export default RetornaAPI;

Veja como ficou o resultado final:

O código acima extrai os nomes da API externa, e em seguida faz a inserção em um estado que está conectado no JSX do componente.

Criando assinaturas de eventos

Uma das possibilidades atreladas ao uso do useEffect refere-se ao fato de que podemos realizar a assinatura de alguns eventos que acontecem durante a execução da nossa aplicação.

Por exemplo, vamos supor que queremos criar um novo componente chamado BotaoClicar, cujo o objetivo é adicionar um EventListener que vai executar um alert sempre quando um usuário clicar em um botão que existe na tela.

BotaoClicar > index.jsx:

import React, { useEffect } from 'react';

const BotaoClicar = () => {
 useEffect(() => {
 const handleClick = () => {
 alert("O botão foi clicado!");
 };

 // Adicionando o event listener para o evento de clique no botão
 document.getElementById('meu-botao').addEventListener('click', handleClick);

 // Função de limpeza para remover o event listener quando o componente for desmontado
 return () => {
 document.getElementById('meu-botao').removeEventListener('click', handleClick);
 };
 }, []); // A dependência está vazia, então o useEffect será executado apenas uma vez

 return (
 <div>
 <h2>Assinatura de Eventos</h2>
 <button id="meu-botao">Clique em mim!</button>
 </div>
 );
};

export default BotaoClicar;

Neste exemplo, estamos adicionando um event listener para o evento de clique em um botão. Quando o botão é clicado, a mensagem "O botão foi clicado!" é exibida no navegador.

Note que removemos a assinatura do evento dentro do return do useEffect, pois se não você terá uma alocação de memoria extra. (É como ter uma assinatura adicionada sempre que o componente não existe mais, ou seja, tá consumindo memoria á toa rs)

Para saber mais sobre o funcionamento desses eventos, não deixe de consultar a lição de DOM.

Todas as assinaturas de eventos que estão relacionadas com determinado componente deverão sempre existir no useEffect?

Não necessariamente. A prática de adicionar assinaturas de eventos diretamente no useEffect é comum quando você precisa garantir que os event listeners sejam adicionados apenas uma vez durante o ciclo de vida do componente e removidos quando o componente é desmontado. 

No entanto, em alguns casos, pode ser preferível adicionar event listeners diretamente nos elementos do DOM no momento em que são renderizados. Isso pode ser feito diretamente no JSX, usando atributos como onClick, onMouseOver, etc. Por exemplo:

import React from 'react';

const EventListenerExample = () => {
 const handleClick = () => {
 alert('O botão foi clicado!');
 };

 return (
 <div>
 <h2>Assinatura de Eventos</h2>
 <button onClick={handleClick}>Clique em mim!</button>
 </div>
 );
};

export default EventListenerExample;

"Mas será que é possível adicionar essas assinaturas no corpo da função? Fora do useEffect?".

Não, pois dê uma olhada nesse exemplo:

const BotaoClicar = () => {
 const handleClick = () => {
 console.log('O botão foi clicado!');
 };

 const button = document.getElementById('meu-botao');
 button.addEventListener('click', handleClick);
 

 return (
 <div>
 <h2>Assinatura de Eventos</h2>
 <button id="meu-botao">Clique em mim!</button>
 </div>
 );
};

export default BotaoClicar;

Um dos motivos do seu código dar erro, está relacionado ao ID 'meu-botao' ainda não existir quando você tenta acessá-lo diretamente fora de um useEffect ou semelhante. Isso ocorre porque o código dentro de um componente é executado antes que o HTML correspondente seja renderizado no DOM, e é por esse motivo que seu código dá erro, pois ele esta tentando localizar um elemento que ainda nem existe (foi montado).

Nesse caso, é recomendável que suas assinaturas de eventos existam dentro do useEffect ou sejam chamadas por meio do onClick, onChange e etc, pois assim garantimos que o nosso componente está montado na tela.

Manipulando o DOM

Uma das caracteristicas do useEffect é que podemos manipular alguns elementos HTML, e isso incluí mudança de cores, espaçamentos e até mesmo a criação de novos elementos.

Para testarmos isso, vamos criar um novo componente chamado de ManipulacaoDOM, cujo o objetivo é mudar as cores de alguns elementos e ainda criar um novo na tela.

ManipulacaoDOM > index.jsx:

import React, { useEffect } from 'react';

const ManipulacaoDOM = () => {
 useEffect(() => {
 // Selecionando um elemento do DOM
 const myElement = document.getElementById('meu-elemento');

 // Alterando o estilo do elemento
 myElement.style.color = 'red';
 myElement.style.fontSize = '20px';

 // Adicionando uma classe ao elemento
 myElement.classList.add('destaque');

 // Criando e adicionando um novo elemento ao DOM
 const newElement = document.createElement('div');
 newElement.textContent = 'Novo elemento adicionado!';
 document.body.appendChild(newElement);

 // Função de limpeza (remover elementos adicionados, reverter alterações, etc.)
 return () => {
 myElement.style.color = ''; // Resetando a cor para o padrão
 myElement.style.fontSize = ''; // Resetando o tamanho da fonte para o padrão
 myElement.classList.remove('destaque'); // Removendo a classe 'destaque'
 document.body.removeChild(newElement); // Removendo o novo elemento adicionado
 };
 }, []); // A dependência está vazia, então o useEffect será executado apenas uma vez

 return (
 <div>
 <h2>Manipulação do DOM</h2>
 <p id="meu-elemento">Este é um elemento manipulado diretamente no DOM.</p>
 </div>
 );
};

export default ManipulacaoDOM;

A primeira vista esse código parece algo super complicado, isto é, se você não passou pela minha jornada Javascript 😅

Neste exemplo, usamos o useEffect para manipular o DOM após o componente ser montado. Dentro do useEffect, selecionamos um elemento do DOM com document.getElementById, e então fizemos alterações diretamente nele, como mudar o seu estilo, adicionar uma classe e adicionar um novo elemento ao corpo do documento usando document.createElement e appendChild.

Note que no final (dentro do return do useEffect) ainda desfazemos todas as alterações para que tudo voltasse ao padrão.

Veja como ficou o resultado final:

Atualizando Estados

Por fim, também foi dito que o useEffect pode ser usado para se atualizar estados da nossa aplicação.

Você se lembra que em lições anteriores eu ensinei a criar um contador usando componentes de classe?

Que tal repetir o mesmo exemplo aqui também? Só que agora usando componentes de estado?

Para isso vamos criar um novo componente chamado de Contador.

Contador > index.jsx:

import React, { useState, useEffect } from 'react';

const Contador= () => {
 const [contador, setContador] = useState(0);

 useEffect(() => {
 // Função para incrementar o contador a cada segundo
 const interval = setInterval(() => {
 setContador(prevContador => prevContador + 1);
 }, 1000);

 // Função de limpeza para parar o intervalo quando o componente for desmontado
 return () => clearInterval(interval);
 }, []); // A dependência está vazia, então o useEffect será executado apenas uma vez

 return (
 <div>
 <h2>Atualizações de Estado com useEffect</h2>
 <p>Contador: {contador}</p>
 </div>
 );
};

export default Contador

Note que o useEffect executa um temporizador que inicia a contagem assim que o componente é montado, onde a cada segundo + 1 é somado no estado.

Veja como ficou o resultado final:

Explorando o segundo argumento do useEffect

O segundo argumento do hook useEffect (e não menos importante) representa uma matriz (array) que controla quando o efeito será executado.

Nós já vimos que quando criamos um useEffect onde não passamos nada dentro do array, o ReactJS entende que a sua execução deve acontecer assim quando o componente é montado na tela, e apenas uma ÚNICA VEZ:

useEffect(() => {
 console.log('Componente foi montado na tela!');
}, []);//É executa assim quando o componente é montado na tela

Vendo com outros olhos  - como um desenvolvedor jQuery, caso você tenha sido um -, essa função é bem parecida com o load() ou o ready() da biblioteca jQuery.

Sendo assim, podemos dizer que se essa matriz estiver vazia, o efeito só será executado após a montagem inicial do componente.

O que se torna muito útil para operações que devem rodar apenas uma única vez.

Porém, se essa mesma matriz contiver variáveis (estados), o efeito será executado novamente sempre que uma dessas variáveis (estados) mudar de valor.

Aham! Então aí nós temos mais uma nova funcionalidade do useEffect 🤓

Para testar essa funcionalidade, vamos criar um novo componente chamado de DespertaEfeito, cujo o objetivo é executar uma função de useEffect toda vez que o usuário clicar em um botão e alterar um determinado estado.

DespertaEfeito > index.jsx:

import React, { useState, useEffect } from 'react';

const DespertaEfeito = () => {
 const [contador, setContador] = useState(0);

 useEffect(() => {
 console.log("Efeito disparado após a alteração do estado contador:", contador);
 }, [contador]);

 const handleClick = () => {
 setContador(prevContador => prevContador + 1);
 };

 return (
 <>
 <button onClick={handleClick}>Clique para aumentar o contador</button>
 <p>Contador: {contador}</p>
 </>
 );
};

export default DespertaEfeito;

Note que toda vez que clicamos no botão (Clique para aumentar o contador), uma mensagem no console é disparada. Onde tal mensagem só existe dentro do useEffect.

Sendo assim, como o useEffect está atrelado com um estado chamado contador, o corpo do useEffect será disparado toda vez que nosso estado for modificado.

Também é possível incluir mais de um estado no array de dependências do useEffect, vamos analisar o exemplo abaixo:

import React, { useState, useEffect } from 'react';

const DespertaEfeito = () => {
 const [contador, setContador] = useState(0);
 const [nome, setNome] = useState('');

 useEffect(() => {
 console.log("Efeito disparado após a alteração do estado contador ou nome:");
 console.log("Contador:", contador);
 console.log("Nome:", nome);
 }, [contador, nome]);

 const handleClick = () => {
 setContador(prevContador => prevContador + 1);
 };

 const handleChange = (event) => {
 setNome(event.target.value);
 };

 return (
 <div>
 <button onClick={handleClick}>Clique para aumentar o contador</button>
 <p>Contador: {contador}</p>
 <input type="text" value={nome} onChange={handleChange} />
 <p>Nome: {nome}</p>
 </div>
 );
};

export default DespertaEfeito;

No código acima, o useEffect está sendo disparado toda vez que o contador ou o nome está sendo modificado.

"Será que tem como saber em qual estado foi modificado?".

Sim, é possível determinar qual dos estados foi modificado dentro do useEffect. Para isso você pode fazer o uso de uma técnica simples com variáveis de referência para acompanhar os valores antigos dos estados e compará-los com os valores atuais, observe:

import React, { useState, useEffect } from 'react';

const DespertaEfeitoDois = () => {
 const [contador, setContador] = useState(0);
 const [nome, setNome] = useState('');
 const [contadorAntigo, setContadorAntigo] = useState(contador);
 const [nomeAntigo, setNomeAntigo] = useState(nome);

 useEffect(() => {
 // Verifica se o estado contador mudou
 if (contador !== contadorAntigo) {
 console.log("O estado contador foi modificado:", contador);
 // Atualiza o estado contadorAntigo com o novo valor do contador
 setContadorAntigo(contador);
 }

 // Verifica se o estado nome mudou
 if (nome !== nomeAntigo) {
 console.log("O estado nome foi modificado:", nome);
 // Atualiza o estado nomeAntigo com o novo valor do nome
 setNomeAntigo(nome);
 }
 }, [contador, nome, contadorAntigo, nomeAntigo]);

 const handleClick = () => {
 setContador(prevContador => prevContador + 1);
 };

 const handleChange = (event) => {
 setNome(event.target.value);
 };

 return (
 <div>
 <button onClick={handleClick}>Clique para aumentar o contador</button>
 <p>Contador: {contador}</p>
 <input type="text" value={nome} onChange={handleChange} />
 <p>Nome: {nome}</p>
 </div>
 );
};

export default DespertaEfeitoDois;

Apesar de não ser uma boa prática, é uma possibilidade, isto é, caso você queira usar um único useEffect e saber qual dos dois estados foi realmente alterado. Pois é muito mais fácil você criar dois useEffect para cada um dos estados, por exemplo:

import React, { useState, useEffect } from 'react';

const DespertaEfeito = () => {
 const [contador, setContador] = useState(0);
 const [nome, setNome] = useState('');

 useEffect(() => {
 console.log("O estado contador foi modificado:", contador);
 }, [contador]);

 useEffect(() => {
 console.log("O estado nome foi modificado:", nome);
 }, [nome]);

 const handleClick = () => {
 setContador(prevContador => prevContador + 1);
 };

 const handleChange = (event) => {
 setNome(event.target.value);
 };

 return (
 <div>
 <button onClick={handleClick}>Clique para aumentar o contador</button>
 <p>Contador: {contador}</p>
 <input type="text" value={nome} onChange={handleChange} />
 <p>Nome: {nome}</p>
 </div>
 );
};

export default DespertaEfeito;

Bem mais fácil e mais limpo, não acha? 😋

Observação: você pode passar quantas variaveis (estados) desejar para dentro do segundo parâmetro do useEffect.

"Vale a pena passar algum estado para dentro do useEffect, mesmo que eu não vá fazer nada com ele?".

Não há necessidade de passá-lo como uma dependência para o array de dependências do useEffect, uma vez que o propósito de incluir variáveis de estado no array de dependências, é para garantir que o useEffect seja executado sempre que essas variáveis mudarem. Se você não está usando o estado dentro do useEffect, não há razão para incluí-lo no array de dependências.

Quando devemos passar variáveis (estados) como segundo parâmetro do useEffect?

Se tratando agora de exemplos reais, há casos em que observar mudanças em estados se torna bem útil:

Carregamento de Dados de API: Ao carregar dados de uma API, você pode querer atualizar o estado de exibição apenas quando os dados forem alterados. Por exemplo, em um aplicativo de previsão do tempo, você pode desejar atualizar a exibição somente quando a localização do usuário mudar.

useEffect(() => {
 fetchWeatherData(location)
 .then(data => setWeather(data))
 .catch(error => setError(error));
}, [location]); // Atualiza apenas quando a localização muda

Pesquisa em Tempo Real: Em uma funcionalidade de pesquisa em tempo real, você pode querer atualizar os resultados da pesquisa somente quando o termo de pesquisa mudar.

useEffect(() => {
 searchDatabase(searchTerm)
 .then(results => setResults(results))
 .catch(error => setError(error));
}, [searchTerm]); // Atualiza apenas quando o termo de pesquisa muda

Autenticação de Usuário: Em um sistema de autenticação, você pode querer redirecionar o usuário para a página de login apenas quando o estado de autenticação mudar.

useEffect(() => {
 if (!isAuthenticated) {
 history.push('/login');
 }
}, [isAuthenticated]); // Redireciona apenas quando o estado de autenticação muda

Controle de Animação: Em uma aplicação com animações, você pode querer iniciar ou parar uma animação apenas quando um estado relacionado à animação mudar.

useEffect(() => {
 if (isAnimating) {
 startAnimation();
 } else {
 stopAnimation();
 }
}, [isAnimating]); // Inicia ou para a animação apenas quando o estado isAnimating muda

Estes foram apenas alguns pequenos exemplos do uso do hook useEffect em conjunto com variáveis (estados) 😉

Importe seus componentes dentro do useEffect sempre quando o corpo da função anônima fizer o uso deles

É exatamente isso o que você leu, até porque o ReactJS alega um erro caso você usar um estado e o mesmo não estiver definido dentro das suas dependências, por exemplo:

import React, { useState, useEffect } from 'react';

const LoopsInfinitos = () => {
 const [contador, setContador] = useState(0);

 useEffect(() => {
 if (contador === 5) {
 alert('O contador atingiu o valor 5!');
 }
 }, []);

 const incrementarContador = () => {
 setContador(prevContador => prevContador + 1);
 };

 return (
 <div>
 <p>Contador: {contador}</p>
 <button onClick={incrementarContador}>Incrementar</button>
 </div>
 );
};

export default LoopsInfinitos;

Observe que no componente LoopsInfinitos, temos um useEffect que faz o uso de um determinado estado ao mesmo tempo que ele não esta declarado dentro do segundo argumento.

O que resulta numa mensagemde alerta gerada pelo próprio ReactJS:

Note que ele pede para que o estado (contador) seja inserido dentro do hook useEffect onde ele esta sendo usado. Para corrigir isso é só importá-lo da seguinte forma:

useEffect(() => {
 if (contador === 5) {
 alert('O contador atingiu o valor 5!');
 }
 }, [contador]);

Caso tivéssemos mais de um estado sendo usado dentro do useEffect, deveriamos importá-los como segundo argumento👍

"E se eu quiser que o useEffect seja executado após a montagem do componente, onde este faz uso de um estado?".

Se você quer que o useEffect seja executado apenas após a montagem do componente e você está usando um estado que é definido na montagem do componente, então você pode passar esse estado como uma dependência vazia para o array de dependências do useEffect.

Isso vai garantir que o useEffect seja executado apenas uma vez, após a montagem do componente. observe:

import React, { useEffect, useState } from 'react';

function MeuComponente() {
 const [meuEstado, setMeuEstado] = useState(null);

 useEffect(() => {
 // Aqui você pode realizar qualquer ação que deseja após a montagem do componente
 // e que dependa do estado meuEstado.
 // Mesmo que você não use 'meuEstado' dentro do useEffect, ainda precisa passá-lo como dependência
 // para garantir que o useEffect seja executado após a montagem e, caso haja mudanças em 'meuEstado' no futuro,
 // o useEffect seja reexecutado.
 // Se você quiser que o useEffect seja executado apenas uma vez, após a montagem, pode passar um array vazio [].
 // Mas se você quer que seja executado uma vez após a montagem e também quando 'meuEstado' mudar, passe [meuEstado].
 
 // Exemplo de uso:
 console.log("Componente montado");
 }, [meuEstado]);

 // Lembre-se de definir o estado 'meuEstado' em algum lugar para garantir que o useEffect seja acionado
 useEffect(() => {
 setMeuEstado('algum-valor');
 }, []);

 return (
 <div>
 {/* Seu componente aqui */}
 </div>
 );
}

No código acima, usamos uma técnica na qual vai chamar o useEffect com depêndencia assim que o componente é montado 😆

Usando o comando return em useEffect com dependências

Também é possível usar um return dentro de um useEffect que possui variáveis com dependências no segundo argumento. Observe:

useEffect(() => {
 // Alguma lógica que depende de variáveis
 console.log('O efeito foi acionado');

 // Função de limpeza específica para o caso de mudança em variáveis específicas
 return () => {
 // Lógica de limpeza a ser executada quando as dependências mudarem
 console.log('A limpeza foi acionada');
 };
}, [variavel1, variavel2]); // Dependências que podem acionar o efeito novamente

Apesar disso, quando o usuário fechar o componente, todos os return de todos os useEffect seriam chamados de uma única vez, o que realizaria uma limpeza completa da sua aplicação.

"Tem lógica usar um return dentro de um useEffect que possui dependência?".

Sim, pois haverá momentos em que o nosso useEffect fará o uso de algum estado ao mesmo tempo que adicionará um listerner, e você como um bom desenvolvedor (é obrigado) a executar um return dentro de um useEffect com dependências.

Como executar operações antes mesmo do componente ser montado na tela?

Haverá alguns casos bem específicos em que a sua aplicação vai precisar realizar algumas operações antes daquele componente ser montado na tela.

Para isso uma alternativa seria utilizar a lógica  fora do componente, em vez de dentro de um hook como o useEffect. No entanto, lembre-se de que essa abordagem está fora do fluxo de vida do componente React:

import React from 'react';

// Código que será executado antes mesmo do componente ser montado
console.log("Antes do componente ser montado");

function MeuComponente() {
 // Código que será executado após a montagem do componente
 console.log("Componente montado");

 return (
 <div>
 {/* Seu componente aqui */}
 </div>
 );
}

export default MeuComponente;

No local onde aparece o console com a mensagem "Antes do componente ser montado", você pode executar diversas operações em Javascript. Mas lembre-se de duas coisas:

1) Qualquer código que você inserir fora da função anônima do seu componente, poderá ser visto por toda a sua aplicação, pois ele se torna global.

2) É importante lembrar que isso está fora do fluxo de vida do componente React.

Apesa disso tudo, o fato de querermos executar operações antes do componente ser montado na tela, vai muito ao encontro de você querer carregar certos dados antes do HTML ser mostrado, estou certo?

Se é realmente isso que você procura, saiba que é possível simular um loading dentro do componente, de modo a fazer com que você não precise inserir códigos fora do componente. E isso pode ser feito usando Renderizações Condicionais.

import React, { useState, useEffect } from 'react';

function MeuComponente() {
 const [loading, setLoading] = useState(true);
 const [users, setUsers] = useState([]);

 useEffect(() => {
 // Simulando uma requisição à API
 fetch('https://jsonplaceholder.typicode.com/users')
 .then(response => response.json())
 .then(data => {
 // Define os usuários e finaliza o estado de carregamento
 setUsers(data);
 setLoading(false);
 })
 .catch(error => {
 console.error('Erro ao obter usuários:', error);
 setLoading(false);
 });
 }, []); // Este useEffect será executado apenas uma vez, após a montagem do componente

 return (
 <div>
 {/* Renderização condicional baseada no estado de carregamento */}
 {loading ? (
 <p>Carregando...</p>
 ) : (
 <div>
 <h1>Lista de Usuários</h1>
 <ul>
 {users.map(user => (
 <li key={user.id}>{user.name}</li>
 ))}
 </ul>
 </div>
 )}
 </div>
 );
}

export default MeuComponente;

No exemplo acima estamos executando uma renderização condicional atrelado a um estado que é responsável por validar se a API carregou os dados ou não.

Se os dados ainda não foram carregados, o componente mostra mensagem: "Carregando...", onde após o carregamento uma lista é mostrada, e mensagem anterior some 😉

Arquivos da lição

Os arquivos dessa lição podem ser encontrados neste repositório do GitHub.

Conclusão

Em resumo o useEffect é um hook que nos ajuda a controlar todo o ciclo de vida da nossa aplicação, se tornando um elemento essêncial nos componentes de estado.

Até a próxima lição 👋