Synchronizing with Effects
Alguns componentes precisam sincronizar com sistemas externos. Por exemplo, você pode querer controlar um componente não-React com base no estado do React, configurar uma conexão de servidor ou enviar um log de análise quando um componente aparece na tela. Efeitos permitem que você execute algum código após a renderização para que possa sincronizar seu componente com algum sistema fora do React.
Você aprenderá
- O que são Efeitos
- Como os Efeitos são diferentes dos eventos
- Como declarar um Efeito em seu componente
- Como pular a reexecução desnecessária de um Efeito
- Por que os Efeitos são executados duas vezes em desenvolvimento e como corrigi-los
O que são Efeitos e como eles são diferentes dos eventos?
Antes de chegar aos Efeitos, você precisa se familiarizar com dois tipos de lógica dentro dos componentes React:
-
Código de renderização (introduzido em Descrevendo a UI) vive no nível superior do seu componente. É aqui que você pega as props e o estado, os transforma e retorna o JSX que deseja ver na tela. O código de renderização deve ser puro. Como uma fórmula matemática, ele deve apenas calcular o resultado, mas não fazer mais nada.
-
Manipuladores de eventos (introduzidos em Adicionando Interatividade) são funções aninhadas dentro de seus componentes que fazem coisas em vez de apenas calculá-las. Um manipulador de eventos pode atualizar um campo de entrada, enviar uma solicitação HTTP POST para comprar um produto ou navegar o usuário para outra tela. Manipuladores de eventos contêm “efeitos colaterais” (eles mudam o estado do programa) causados por uma ação específica do usuário (por exemplo, um clique de botão ou digitação).
Às vezes, isso não é suficiente. Considere um componente ChatRoom que precisa se conectar ao servidor de chat sempre que estiver visível na tela. Conectar-se a um servidor não é um cálculo puro (é um efeito colateral), portanto, não pode ocorrer durante a renderização. No entanto, não há um único evento específico, como um clique, que cause a exibição do ChatRoom.
Efeitos permitem que você especifique efeitos colaterais que são causados pela própria renderização, em vez de por um evento específico. Enviar uma mensagem no chat é um evento porque é diretamente causado pelo usuário clicando em um botão específico. No entanto, configurar uma conexão de servidor é um Efeito porque deve acontecer independentemente de qual interação causou o aparecimento do componente. Efeitos são executados no final de um commit após a atualização da tela. Este é um bom momento para sincronizar os componentes React com algum sistema externo (como rede ou uma biblioteca de terceiros).
Você pode não precisar de um Efeito
Não se apresse em adicionar Efeitos aos seus componentes. Tenha em mente que os Efeitos são tipicamente usados para “sair” do seu código React e sincronizar com algum sistema externo. Isso inclui APIs do navegador, widgets de terceiros, rede e assim por diante. Se o seu Efeito apenas ajusta algum estado com base em outro estado, você pode não precisar de um Efeito.
Como escrever um Efeito
Para escrever um Efeito, siga estas três etapas:
- Declare um Efeito. Por padrão, seu Efeito será executado após cada commit.
- Especifique as dependências do Efeito. A maioria dos Efeitos deve ser reexecutada apenas quando necessário, em vez de após cada renderização. Por exemplo, uma animação de fade-in deve ser acionada apenas quando um componente aparece. Conectar e desconectar de uma sala de chat só deve acontecer quando o componente aparece e desaparece, ou quando a sala de chat muda. Você aprenderá como controlar isso especificando dependências.
- Adicione limpeza, se necessário. Alguns Efeitos precisam especificar como parar, desfazer ou limpar o que quer que estivessem fazendo. Por exemplo, “conectar” precisa de “desconectar”, “inscrever” precisa de “cancelar inscrição” e “buscar” precisa de “cancelar” ou “ignorar”. Você aprenderá como fazer isso retornando uma função de limpeza.
Vamos analisar cada uma dessas etapas em detalhes.
Etapa 1: Declare um Efeito
Para declarar um Efeito em seu componente, importe o Hook useEffect do React:
import { useEffect } from 'react';Em seguida, chame-o no nível superior do seu componente e coloque algum código dentro do seu Efeito:
function MyComponent() {
useEffect(() => {
// O código aqui será executado após *cada* renderização
});
return <div />;
}Toda vez que seu componente renderizar, o React atualizará a tela e então executará o código dentro do useEffect. Em outras palavras, useEffect “atrasa” a execução de um trecho de código até que essa renderização seja refletida na tela.
Vamos ver como você pode usar um Efeito para sincronizar com um sistema externo. Considere um componente React <VideoPlayer>. Seria bom controlar se ele está tocando ou pausado passando uma prop isPlaying para ele:
<VideoPlayer isPlaying={isPlaying} />;Seu componente VideoPlayer personalizado renderiza a tag <video> integrada do navegador:
function VideoPlayer({ src, isPlaying }) {
// TODO: fazer algo com isPlaying
return <video src={src} />;
}No entanto, a tag <video> do navegador não tem uma prop isPlaying. A única maneira de controlá-la é chamar manualmente os métodos play() e pause() no elemento DOM. Você precisa sincronizar o valor da prop isPlaying, que indica se o vídeo deve estar tocando no momento, com chamadas como play() e pause().
Precisaremos primeiro obter uma ref para o nó DOM do <video>.
Você pode ser tentado a tentar chamar play() ou pause() durante a renderização, mas isso não está correto:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); if (isPlaying) { ref.current.play(); // Chamar isso durante a renderização não é permitido. } else { ref.current.pause(); // Além disso, isso trava. } return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
A razão pela qual este código não está correto é que ele tenta fazer algo com o nó DOM durante a renderização. No React, a renderização deve ser um cálculo puro de JSX e não deve conter efeitos colaterais como a modificação do DOM.
Além disso, quando VideoPlayer é chamado pela primeira vez, seu DOM ainda não existe! Ainda não há um nó DOM para chamar play() ou pause() nele, porque o React não sabe qual DOM criar até que você retorne o JSX.
A solução aqui é envolver o efeito colateral com useEffect para removê-lo do cálculo de renderização:
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}Ao envolver a atualização do DOM em um Efeito, você permite que o React atualize a tela primeiro. Em seguida, seu Efeito é executado.
Quando seu componente VideoPlayer renderizar (seja pela primeira vez ou se re-renderizar), algumas coisas acontecerão. Primeiro, o React atualizará a tela, garantindo que a tag <video> esteja no DOM com as props corretas. Em seguida, o React executará seu Efeito. Finalmente, seu Efeito chamará play() ou pause() dependendo do valor de isPlaying.
Pressione Play/Pause várias vezes e veja como o player de vídeo permanece sincronizado com o valor de isPlaying:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
Neste exemplo, o “sistema externo” ao qual você sincronizou o estado do React foi a API de mídia do navegador. Você pode usar uma abordagem semelhante para envolver código legado não-React (como plugins jQuery) em componentes React declarativos.
Note que controlar um player de vídeo é muito mais complexo na prática. Chamar play() pode falhar, o usuário pode reproduzir ou pausar usando os controles integrados do navegador, e assim por diante. Este exemplo é muito simplificado e incompleto.
Etapa 2: Especifique as dependências do Efeito
Por padrão, os Efeitos são executados após cada renderização. Frequentemente, isso não é o que você quer:
- Às vezes, é lento. Sincronizar com um sistema externo nem sempre é instantâneo, então você pode querer pular a execução, a menos que seja necessário. Por exemplo, você não quer reconectar ao servidor de chat a cada pressionamento de tecla.
- Às vezes, está errado. Por exemplo, você não quer acionar uma animação de fade-in do componente a cada pressionamento de tecla. A animação só deve ser reproduzida uma vez quando o componente aparece pela primeira vez.
Para demonstrar o problema, aqui está o exemplo anterior com algumas chamadas console.log e um campo de texto que atualiza o estado do componente pai. Observe como digitar faz com que o Efeito seja reexecutado:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('Calling video.play()'); ref.current.play(); } else { console.log('Calling video.pause()'); ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
Você pode dizer ao React para pular a reexecução desnecessária do Efeito especificando um array de dependências como segundo argumento para a chamada useEffect. Comece adicionando um array vazio [] ao exemplo acima na linha 14:
useEffect(() => {
// ...
}, []);Você verá um erro dizendo React Hook useEffect has a missing dependency: 'isPlaying':
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('Calling video.play()'); ref.current.play(); } else { console.log('Calling video.pause()'); ref.current.pause(); } }, []); // Isso causa um erro return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
O problema é que o código dentro do seu Efeito depende da prop isPlaying para decidir o que fazer, mas essa dependência não foi explicitamente declarada. Para corrigir esse problema, adicione isPlaying ao array de dependências:
useEffect(() => {
if (isPlaying) { // É usado aqui...
// ...
} else {
// ...
}
}, [isPlaying]); // ...então deve ser declarado aqui!Agora todas as dependências estão declaradas, então não há erro. Especificar [isPlaying] como o array de dependências diz ao React que ele deve pular a reexecução do seu Efeito se isPlaying for o mesmo que era durante a renderização anterior. Com essa alteração, digitar no input não faz o Efeito ser reexecutado, mas pressionar Play/Pause sim:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('Calling video.play()'); ref.current.play(); } else { console.log('Calling video.pause()'); ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
O array de dependências pode conter várias dependências. O React só pulará a reexecução do Efeito se todas as dependências que você especificar tiverem exatamente os mesmos valores que tinham durante a renderização anterior. O React compara os valores de dependência usando a comparação Object.is. Consulte a referência do useEffect para obter detalhes.
Observe que você não pode “escolher” suas dependências. Você receberá um erro de lint se as dependências que você especificou não corresponderem ao que o React espera com base no código dentro do seu Efeito. Isso ajuda a capturar muitos bugs em seu código. Se você não quiser que algum código seja reexecutado, edite o próprio código do Efeito para não “precisar” dessa dependência.
Deep Dive
Este Efeito usa tanto ref quanto isPlaying, mas apenas isPlaying é declarado como dependência:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);Isso ocorre porque o objeto ref tem uma identidade estável: o React garante que você sempre receberá o mesmo objeto da mesma chamada useRef em cada renderização. Ele nunca muda, portanto, nunca causará por si só a reexecução do Efeito. Portanto, não importa se você o inclui ou não. Incluí-lo também é bom:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);As funções set retornadas por useState também têm identidade estável, então você frequentemente as verá omitidas das dependências também. Se o linter permitir que você omita uma dependência sem erros, é seguro fazê-lo.
Omitir dependências sempre estáveis só funciona quando o linter pode “ver” que o objeto é estável. Por exemplo, se ref fosse passado de um componente pai, você teria que especificá-lo no array de dependências. No entanto, isso é bom porque você não pode saber se o componente pai sempre passa a mesma ref, ou passa uma de várias refs condicionalmente. Portanto, seu Efeito dependeria de qual ref é passada.
Etapa 3: Adicione limpeza, se necessário
Considere um exemplo diferente. Você está escrevendo um componente ChatRoom que precisa se conectar ao servidor de chat quando ele aparece. Você recebeu uma API createConnection() que retorna um objeto com os métodos connect() e disconnect(). Como você mantém o componente conectado enquanto ele está visível para o usuário?
Comece escrevendo a lógica do Efeito:
useEffect(() => {
const connection = createConnection();
connection.connect();
});Seria lento conectar ao chat após cada re-renderização, então você adiciona o array de dependências:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);O código dentro do Efeito não usa nenhuma prop ou estado, então seu array de dependências é [] (vazio). Isso diz ao React para executar esse código apenas quando o componente “montar”, ou seja, aparecer na tela pela primeira vez.
Vamos tentar executar este código:
import { useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); }, []); return <h1>Welcome to the chat!</h1>; }
Este Efeito só é executado na montagem, então você pode esperar que "✅ Connecting..." seja impresso uma vez no console. No entanto, se você verificar o console, "✅ Connecting..." é impresso duas vezes. Por que isso acontece?
Imagine que o componente ChatRoom faz parte de um aplicativo maior com muitas telas diferentes. O usuário inicia sua jornada na página ChatRoom. O componente monta e chama connection.connect(). Em seguida, imagine que o usuário navega para outra tela - por exemplo, para a página de Configurações. O componente ChatRoom desmonta. Finalmente, o usuário clica em Voltar e ChatRoom monta novamente. Isso configuraria uma segunda conexão - mas a primeira conexão nunca foi destruída! À medida que o usuário navega pelo aplicativo, as conexões continuariam a se acumular.
Bugs como esse são fáceis de perder sem testes manuais extensivos. Para ajudá-lo a identificá-los rapidamente, em desenvolvimento, o React remonta cada componente uma vez imediatamente após sua montagem inicial.
Ver o log "✅ Connecting..." duas vezes ajuda você a perceber o problema real: seu código não fecha a conexão quando o componente desmonta.
Para corrigir o problema, retorne uma função de limpeza do seu Efeito:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);O React chamará sua função de limpeza cada vez antes que o Efeito seja executado novamente, e uma última vez quando o componente desmontar (for removido). Vamos ver o que acontece quando a função de limpeza é implementada:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the chat!</h1>; }
Agora você obtém três logs no console em desenvolvimento:
"✅ Connecting...""❌ Disconnected.""✅ Connecting..."
Este é o comportamento correto em desenvolvimento. Ao remontar seu componente, o React verifica se a navegação para frente e para trás não quebraria seu código. Desconectar e depois conectar novamente é exatamente o que deveria acontecer! Quando você implementa a limpeza corretamente, não deve haver diferença visível para o usuário entre executar o Efeito uma vez, limpá-lo e executá-lo novamente. Há um par extra de chamadas de conectar/desconectar porque o React está sondando seu código em busca de bugs em desenvolvimento. Isso é normal - não tente fazer com que desapareça!
Em produção, você veria apenas "✅ Connecting..." impresso uma vez. A remontagem de componentes só acontece em desenvolvimento para ajudá-lo a encontrar Efeitos que precisam de limpeza. Você pode desativar o Modo Estrito para optar por não participar do comportamento de desenvolvimento, mas recomendamos mantê-lo ativado. Isso permite que você encontre muitos bugs como o acima.
Como lidar com o Effect disparando duas vezes em desenvolvimento?
O React intencionalmente remonta seus componentes em desenvolvimento para encontrar bugs como no último exemplo. A pergunta correta não é “como executar um Effect uma vez”, mas sim “como corrigir meu Effect para que ele funcione após a remontagem”.
Geralmente, a resposta é implementar a função de limpeza. A função de limpeza deve parar ou desfazer o que quer que o Effect estivesse fazendo. A regra geral é que o usuário não deve ser capaz de distinguir entre o Effect sendo executado uma vez (como na produção) e uma sequência de configuração → limpeza → configuração (como você veria em desenvolvimento).
A maioria dos Effects que você escrever se encaixará em um dos padrões comuns abaixo.
Controlando widgets não-React
Às vezes, você precisa adicionar widgets de UI que não foram escritos em React. Por exemplo, digamos que você esteja adicionando um componente de mapa à sua página. Ele tem um método setZoomLevel(), e você gostaria de manter o nível de zoom sincronizado com uma variável de estado zoomLevel em seu código React. Seu Effect se pareceria com isto:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);Note que não há necessidade de limpeza neste caso. Em desenvolvimento, o React chamará o Effect duas vezes, mas isso não é um problema porque chamar setZoomLevel duas vezes com o mesmo valor não faz nada. Pode ser um pouco mais lento, mas isso não importa porque não haverá remontagem desnecessária em produção.
Algumas APIs podem não permitir que você as chame duas vezes seguidas. Por exemplo, o método showModal do elemento <dialog> embutido lança um erro se você o chamar duas vezes. Implemente a função de limpeza e faça-a fechar a caixa de diálogo:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);Em desenvolvimento, seu Effect chamará showModal(), depois imediatamente close(), e então showModal() novamente. Isso tem o mesmo comportamento visível para o usuário que chamar showModal() uma vez, como você veria em produção.
Assinando eventos
Se o seu Effect assina algo, a função de limpeza deve cancelar a assinatura:
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);Em desenvolvimento, seu Effect chamará addEventListener(), depois imediatamente removeEventListener(), e então addEventListener() novamente com o mesmo manipulador. Assim, haverá apenas uma assinatura ativa por vez. Isso tem o mesmo comportamento visível para o usuário que chamar addEventListener() uma vez, como em produção.
Acionando animações
Se o seu Effect anima algo, a função de limpeza deve redefinir a animação para os valores iniciais:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Aciona a animação
return () => {
node.style.opacity = 0; // Redefine para o valor inicial
};
}, []);Em desenvolvimento, a opacidade será definida para 1, depois para 0, e então para 1 novamente. Isso deve ter o mesmo comportamento visível para o usuário que defini-la para 1 diretamente, que é o que aconteceria em produção. Se você usar uma biblioteca de animação de terceiros com suporte para tweening, sua função de limpeza deve redefinir a linha do tempo para seu estado inicial.
Buscando dados
Se o seu Effect busca algo, a função de limpeza deve abortar o fetch ou ignorar seu resultado:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);Você não pode “desfazer” uma requisição de rede que já ocorreu, mas sua função de limpeza deve garantir que o fetch que não é mais relevante não continue afetando sua aplicação. Se o userId mudar de 'Alice' para 'Bob', a limpeza garante que a resposta de 'Alice' seja ignorada, mesmo que chegue depois de 'Bob'.
Em desenvolvimento, você verá dois fetches na aba Network. Não há nada de errado com isso. Com a abordagem acima, o primeiro Effect será imediatamente limpo, de modo que sua cópia da variável ignore será definida como true. Portanto, embora haja uma requisição extra, ela não afetará o estado graças à verificação if (!ignore).
Em produção, haverá apenas uma requisição. Se a segunda requisição em desenvolvimento estiver incomodando você, a melhor abordagem é usar uma solução que deduplique requisições e armazene em cache suas respostas entre os componentes:
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...Isso não apenas melhorará a experiência de desenvolvimento, mas também tornará sua aplicação mais rápida. Por exemplo, o usuário que pressiona o botão Voltar não terá que esperar que alguns dados sejam carregados novamente, pois eles estarão em cache. Você pode criar um cache assim ou usar uma das muitas alternativas para buscar dados manualmente em Effects.
Deep Dive
Escrever chamadas fetch dentro de Effects é uma maneira popular de buscar dados, especialmente em aplicativos totalmente do lado do cliente. Esta, no entanto, é uma abordagem muito manual e tem desvantagens significativas:
- Effects não são executados no servidor. Isso significa que o HTML renderizado inicialmente pelo servidor incluirá apenas um estado de carregamento sem dados. O computador do cliente terá que baixar todo o JavaScript e renderizar seu aplicativo apenas para descobrir que agora ele precisa carregar os dados. Isso não é muito eficiente.
- Buscar dados diretamente em Effects facilita a criação de “quedas de rede”. Você renderiza o componente pai, ele busca alguns dados, renderiza os componentes filhos, e então eles começam a buscar seus dados. Se a rede não for muito rápida, isso é significativamente mais lento do que buscar todos os dados em paralelo.
- Buscar dados diretamente em Effects geralmente significa que você não pré-carrega ou armazena dados em cache. Por exemplo, se o componente desmontar e depois montar novamente, ele terá que buscar os dados novamente.
- Não é muito ergonômico. Há uma quantidade considerável de código repetitivo envolvido ao escrever chamadas
fetchde uma maneira que não sofra de bugs como condições de corrida.
Esta lista de desvantagens não é específica do React. Ela se aplica à busca de dados na montagem com qualquer biblioteca. Assim como no roteamento, buscar dados não é trivial de fazer bem, então recomendamos as seguintes abordagens:
- Se você usa um framework, use seu mecanismo de busca de dados embutido. Frameworks React modernos têm mecanismos de busca de dados integrados que são eficientes e não sofrem das armadilhas acima.
- Caso contrário, considere usar ou construir um cache do lado do cliente. Soluções populares de código aberto incluem TanStack Query, useSWR e React Router 6.4+. Você também pode construir sua própria solução, caso em que usaria Effects internamente, mas adicionaria lógica para deduplicar requisições, armazenar em cache respostas e evitar quedas de rede (pré-carregando dados ou elevando requisitos de dados para rotas).
Você pode continuar buscando dados diretamente em Effects se nenhuma dessas abordagens for adequada para você.
Enviando análises
Considere este código que envia um evento de análise na visita à página:
useEffect(() => {
logVisit(url); // Envia uma requisição POST
}, [url]);Em desenvolvimento, logVisit será chamado duas vezes para cada URL, então você pode ser tentado a tentar corrigir isso. Recomendamos manter este código como está. Assim como nos exemplos anteriores, não há diferença de comportamento visível para o usuário entre executá-lo uma vez e executá-lo duas vezes. Do ponto de vista prático, logVisit não deve fazer nada em desenvolvimento, pois você não quer que os logs das máquinas de desenvolvimento distorçam as métricas de produção. Seu componente é remontado toda vez que você salva seu arquivo, então ele registra visitas extras em desenvolvimento de qualquer maneira.
Em produção, não haverá logs de visita duplicados.
Para depurar os eventos de análise que você está enviando, você pode implantar seu aplicativo em um ambiente de staging (que é executado em modo de produção) ou optar temporariamente por Strict Mode e suas verificações de remontagem apenas em desenvolvimento. Você também pode enviar análises dos manipuladores de eventos de mudança de rota em vez de Effects. Para análises mais precisas, observadores de interseção podem ajudar a rastrear quais componentes estão na viewport e por quanto tempo permanecem visíveis.
Não é um Effect: Inicializando a aplicação
Alguma lógica deve ser executada apenas uma vez quando a aplicação inicia. Você pode colocá-la fora de seus componentes:
if (typeof window !== 'undefined') { // Verifica se estamos executando no navegador.
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}Isso garante que tal lógica seja executada apenas uma vez após o navegador carregar a página.
Não é um Effect: Comprando um produto
Às vezes, mesmo que você escreva uma função de limpeza, não há como evitar as consequências visíveis para o usuário de executar o Effect duas vezes. Por exemplo, talvez seu Effect envie uma requisição POST como a compra de um produto:
useEffect(() => {
// 🔴 Errado: Este Effect dispara duas vezes em desenvolvimento, expondo um problema no código.
fetch('/api/buy', { method: 'POST' });
}, []);Você não gostaria de comprar o produto duas vezes. No entanto, é por isso que você não deve colocar essa lógica em um Effect. E se o usuário for para outra página e depois pressionar Voltar? Seu Effect seria executado novamente. Você não quer comprar o produto quando o usuário visita uma página; você quer comprá-lo quando o usuário clica no botão Comprar.
Comprar não é causado pela renderização; é causado por uma interação específica. Deve ser executado apenas quando o usuário pressiona o botão. Exclua o Effect e mova sua requisição /api/buy para o manipulador de eventos do botão Comprar:
function handleClick() {
// ✅ Comprar é um evento porque é causado por uma interação específica.
fetch('/api/buy', { method: 'POST' });
}Isso ilustra que, se a remontagem quebrar a lógica da sua aplicação, isso geralmente descobre bugs existentes. Da perspectiva do usuário, visitar uma página não deve ser diferente de visitá-la, clicar em um link e depois pressionar Voltar para ver a página novamente. O React verifica se seus componentes cumprem esse princípio, remontando-os uma vez em desenvolvimento.
Juntando tudo
Este playground pode ajudar você a “sentir” como os Efeitos funcionam na prática.
Este exemplo usa setTimeout para agendar um log no console com o texto de entrada para aparecer três segundos após a execução do Efeito. A função de limpeza cancela o timeout pendente. Comece pressionando “Montar o componente”:
import { useState, useEffect } from 'react'; function Playground() { const [text, setText] = useState('a'); useEffect(() => { function onTimeout() { console.log('⏰ ' + text); } console.log('🔵 Agendar log de "' + text + '"'); const timeoutId = setTimeout(onTimeout, 3000); return () => { console.log('🟡 Cancelar log de "' + text + '"'); clearTimeout(timeoutId); }; }, [text]); return ( <> <label> O que registrar:{' '} <input value={text} onChange={e => setText(e.target.value)} /> </label> <h1>{text}</h1> </> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Desmontar' : 'Montar'} o componente </button> {show && <hr />} {show && <Playground />} </> ); }
Você verá três logs inicialmente: Agendar log de "a", Cancelar log de "a" e Agendar log de "a" novamente. Três segundos depois, haverá também um log dizendo a. Como você aprendeu anteriormente, o par extra de agendamento/cancelamento ocorre porque o React remonta o componente uma vez em desenvolvimento para verificar se você implementou a limpeza corretamente.
Agora edite a entrada para que diga abc. Se você fizer isso rápido o suficiente, verá Agendar log de "ab" imediatamente seguido por Cancelar log de "ab" e Agendar log de "abc". O React sempre limpa o Efeito do render anterior antes do Efeito do próximo render. É por isso que, mesmo que você digite rapidamente na entrada, haverá no máximo um timeout agendado por vez. Edite a entrada algumas vezes e observe o console para ter uma ideia de como os Efeitos são limpos.
Digite algo na entrada e pressione imediatamente “Desmontar o componente”. Observe como desmontar limpa o Efeito do último render. Aqui, ele cancela o último timeout antes que ele tenha a chance de disparar.
Finalmente, edite o componente acima e comente a função de limpeza para que os timeouts não sejam cancelados. Tente digitar abcde rapidamente. O que você espera que aconteça em três segundos? console.log(text) dentro do timeout imprimirá o text mais recente e produzirá cinco logs de abcde? Tente para verificar sua intuição!
Três segundos depois, você deverá ver uma sequência de logs (a, ab, abc, abcd e abcde) em vez de cinco logs de abcde. Cada Efeito “captura” o valor text de seu render correspondente. Não importa que o estado text tenha mudado: um Efeito do render com text = 'ab' sempre verá 'ab'. Em outras palavras, os Efeitos de cada render são isolados uns dos outros. Se você estiver curioso sobre como isso funciona, pode ler sobre closures.
Deep Dive
Você pode pensar em useEffect como “anexar” um pedaço de comportamento à saída do render. Considere este Efeito:
export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>Welcome to {roomId}!</h1>;
}Vamos ver o que exatamente acontece enquanto o usuário navega pelo aplicativo.
Render inicial
O usuário visita <ChatRoom roomId="general" />. Vamos substituir mentalmente roomId por 'general':
// JSX para o primeiro render (roomId = "general")
return <h1>Welcome to general!</h1>;O Efeito também é parte da saída do render. O Efeito do primeiro render se torna:
// Efeito para o primeiro render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependências para o primeiro render (roomId = "general")
['general']O React executa este Efeito, que se conecta à sala de chat 'general'.
Re-render com as mesmas dependências
Digamos que <ChatRoom roomId="general" /> seja renderizado novamente. A saída JSX é a mesma:
// JSX para o segundo render (roomId = "general")
return <h1>Welcome to general!</h1>;O React vê que a saída do render não mudou, então ele não atualiza o DOM.
O Efeito do segundo render se parece com isto:
// Efeito para o segundo render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependências para o segundo render (roomId = "general")
['general']O React compara ['general'] do segundo render com ['general'] do primeiro render. Como todas as dependências são as mesmas, o React ignora o Efeito do segundo render. Ele nunca é chamado.
Re-render com dependências diferentes
Em seguida, o usuário visita <ChatRoom roomId="travel" />. Desta vez, o componente retorna um JSX diferente:
// JSX para o terceiro render (roomId = "travel")
return <h1>Welcome to travel!</h1>;O React atualiza o DOM para mudar "Welcome to general" para "Welcome to travel".
O Efeito do terceiro render se parece com isto:
// Efeito para o terceiro render (roomId = "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
// Dependências para o terceiro render (roomId = "travel")
['travel']O React compara ['travel'] do terceiro render com ['general'] do segundo render. Uma dependência é diferente: Object.is('travel', 'general') é false. O Efeito não pode ser ignorado.
Antes que o React possa aplicar o Efeito do terceiro render, ele precisa limpar o último Efeito que foi executado. O Efeito do segundo render foi ignorado, então o React precisa limpar o Efeito do primeiro render. Se você rolar para cima até o primeiro render, verá que sua limpeza chama disconnect() na conexão que foi criada com createConnection('general'). Isso desconecta o aplicativo da sala de chat 'general'.
Depois disso, o React executa o Efeito do terceiro render. Ele se conecta à sala de chat 'travel'.
Desmontar
Finalmente, digamos que o usuário navegue para longe e o componente ChatRoom seja desmontado. O React executa a função de limpeza do último Efeito. O último Efeito foi do terceiro render. A limpeza do terceiro render destrói a conexão createConnection('travel'). Assim, o aplicativo se desconecta da sala 'travel'.
Comportamentos exclusivos do desenvolvimento
Quando o Modo Estrito está ativado, o React remonta cada componente uma vez após a montagem (estado e DOM são preservados). Isso ajuda você a encontrar Efeitos que precisam de limpeza e expõe bugs como condições de corrida precocemente. Além disso, o React remontará os Efeitos sempre que você salvar um arquivo em desenvolvimento. Ambos esses comportamentos são exclusivos do desenvolvimento.
Recap
- Ao contrário dos eventos, os Efeitos são causados pelo próprio render, e não por uma interação específica.
- Os Efeitos permitem sincronizar um componente com algum sistema externo (API de terceiros, rede, etc.).
- Por padrão, os Efeitos são executados após cada render (incluindo o inicial).
- O React ignorará o Efeito se todas as suas dependências tiverem os mesmos valores da última renderização.
- Você não pode “escolher” suas dependências. Elas são determinadas pelo código dentro do Efeito.
- Um array de dependências vazio (
[]) corresponde à “montagem” do componente, ou seja, à sua adição à tela. - No Modo Estrito, o React monta os componentes duas vezes (apenas em desenvolvimento!) para testar seus Efeitos.
- Se o seu Efeito falhar devido à remontagem, você precisará implementar uma função de limpeza.
- O React chamará sua função de limpeza antes que o Efeito seja executado na próxima vez e durante o desmontar.
Challenge 1 of 4: Focar um campo na montagem
Neste exemplo, o formulário renderiza um componente <MyInput />.
Use o método focus() da entrada para fazer MyInput focar automaticamente quando ele aparecer na tela. Já existe uma implementação comentada, mas ela não funciona exatamente. Descubra por que não funciona e corrija-a. (Se você estiver familiarizado com o atributo autoFocus, finja que ele não existe: estamos reimplementando a mesma funcionalidade do zero.)
import { useEffect, useRef } from 'react'; export default function MyInput({ value, onChange }) { const ref = useRef(null); // TODO: Isso não funciona exatamente. Corrija. // ref.current.focus() return ( <input ref={ref} value={value} onChange={onChange} /> ); }
Para verificar se sua solução funciona, pressione “Mostrar formulário” e verifique se a entrada recebe foco (fica destacada e o cursor é colocado dentro). Pressione “Ocultar formulário” e “Mostrar formulário” novamente. Verifique se a entrada está destacada novamente.
MyInput deve focar apenas na montagem, e não após cada renderização. Para verificar se o comportamento está correto, pressione “Mostrar formulário” e, em seguida, pressione repetidamente a caixa de seleção “Tornar maiúsculo”. Clicar na caixa de seleção não deve focar a entrada acima dela.