Separando Eventos de Efeitos
Manipuladores de eventos só são executados novamente quando você realiza a mesma interação novamente. Ao contrário dos manipuladores de eventos, os Efeitos são re-sincronizados se algum valor que eles leem, como uma prop ou uma variável de estado, for diferente do que era durante a última renderização. Às vezes, você também quer uma mistura de ambos os comportamentos: um Efeito que é executado em resposta a alguns valores, mas não a outros. Esta página ensinará como fazer isso.
Você aprenderá
- Como escolher entre um manipulador de eventos e um Efeito
- Por que os Efeitos são reativos e os manipuladores de eventos não
- O que fazer quando você quer que uma parte do código do seu Efeito não seja reativa
- O que são Eventos de Efeito e como extraí-los dos seus Efeitos
- Como ler as últimas props e estado dos Efeitos usando Eventos de Efeito
Escolhendo entre manipuladores de eventos e Efeitos
Primeiro, vamos revisar a diferença entre manipuladores de eventos e Efeitos.
Imagine que você está implementando um componente de chat. Seus requisitos são os seguintes:
- Seu componente deve se conectar automaticamente à sala de chat selecionada.
- Quando você clicar no botão “Enviar”, ele deve enviar uma mensagem para o chat.
Vamos supor que você já implementou o código para eles, mas não tem certeza de onde colocá-lo. Você deve usar manipuladores de eventos ou Efeitos? Sempre que precisar responder a essa pergunta, considere por que o código precisa ser executado.
Manipuladores de eventos são executados em resposta a interações específicas
Do ponto de vista do usuário, enviar uma mensagem deve acontecer porque o botão “Enviar” específico foi clicado. O usuário ficará bastante chateado se você enviar a mensagem dele em qualquer outro momento ou por qualquer outro motivo. É por isso que enviar uma mensagem deve ser um manipulador de eventos. Os manipuladores de eventos permitem que você trate interações específicas:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Enviar</button>
</>
);
}
Com um manipulador de eventos, você pode ter certeza de que sendMessage(message)
será executado apenas se o usuário pressionar o botão.
Efeitos são executados sempre que a sincronização é necessária
Lembre-se de que você também precisa manter o componente conectado à sala de chat. Onde esse código deve ir?
A razão para executar esse código não é alguma interação específica. Não importa como ou por que o usuário navegou até a tela da sala de chat. Agora que está olhando para ela e poderia interagir com ela, o componente precisa permanecer conectado ao servidor de chat selecionado. Mesmo que o componente da sala de chat fosse a tela inicial do seu aplicativo, e o usuário não tivesse realizado nenhuma interação, você ainda precisaria se conectar. É por isso que é um Efeito:
function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
Com esse código, você pode ter certeza de que sempre há uma conexão ativa com o servidor de chat atualmente selecionado, independentemente das interações específicas realizadas pelo usuário. Quer o usuário tenha apenas aberto seu aplicativo, selecionado uma sala diferente, ou navegado para outra tela e voltado, seu Efeito garante que o componente permaneça sincronizado com a sala atualmente selecionada, e reconectará sempre que necessário.
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); function handleSendClick() { sendMessage(message); } return ( <> <h1>Bem-vindo à sala {roomId}!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> <button onClick={handleSendClick}>Enviar</button> </> ); } export default function App() { const [roomId, setRoomId] = useState('geral'); const [show, setShow] = useState(false); return ( <> <label> Escolha a sala de chat:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="geral">geral</option> <option value="viagem">viagem</option> <option value="musica">música</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Fechar chat' : 'Abrir chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Valores reativos e lógica reativa
Intuitivamente, você poderia dizer que os manipuladores de eventos são sempre disparados “manualmente”, por exemplo, clicando em um botão. Os Efeitos, por outro lado, são “automáticos”: eles são executados e re-executados sempre que necessário para permanecerem sincronizados.
Há uma maneira mais precisa de pensar sobre isso.
Props, estado e variáveis declaradas dentro do corpo do seu componente são chamadas de valores reativos. Neste exemplo, serverUrl
não é um valor reativo, mas roomId
e message
são. Eles participam do fluxo de dados de renderização:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}
Valores reativos como esses podem mudar devido a uma nova renderização. Por exemplo, o usuário pode editar a message
ou escolher um roomId
diferente em um dropdown. Manipuladores de eventos e Efeitos respondem às mudanças de maneira diferente:
- A lógica dentro dos manipuladores de eventos não é reativa. Ela não será executada novamente, a menos que o usuário realize a mesma interação (por exemplo, um clique) novamente. Os manipuladores de eventos podem ler valores reativos sem “reagir” às suas mudanças.
- A lógica dentro dos Efeitos é reativa. Se seu Efeito ler um valor reativo, você precisa especificá-lo como uma dependência. Então, se uma nova renderização fizer com que esse valor mude, o React re-executará a lógica do seu Efeito com o novo valor.
Vamos revisar o exemplo anterior para ilustrar essa diferença.
A lógica dentro dos manipuladores de eventos não é reativa
Veja esta linha de código. Essa lógica deve ser reativa ou não?
// ...
sendMessage(message);
// ...
Do ponto de vista do usuário, uma mudança na message
não significa que eles querem enviar uma mensagem. Isso apenas significa que o usuário está digitando. Em outras palavras, a lógica que envia uma mensagem não deve ser reativa. Ela não deve ser executada novamente apenas porque o valor reativo mudou. É por isso que pertence ao manipulador de eventos:
function handleSendClick() {
sendMessage(message);
}
Os manipuladores de eventos não são reativos, então sendMessage(message)
só será executado quando o usuário clicar no botão Enviar.
A lógica dentro dos Efeitos é reativa
Agora vamos voltar para essas linhas:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...
Do ponto de vista do usuário, uma mudança no roomId
significa que eles querem se conectar a uma sala diferente. Em outras palavras, a lógica para se conectar à sala deve ser reativa. Você quer que essas linhas de código “acompanhem” o valor reativo, e sejam executadas novamente se ese valor for diferente. É por isso que pertence a um Efeito:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
Os Efeitos são reativos, então createConnection(serverUrl, roomId)
e connection.connect()
serão executados para cada valor distinto de roomId
. Seu Efeito mantém a conexão de chat sincronizada com a sala atualmente selecionada.
Extraindo lógica não reativa dos Efeitos
As coisas ficam mais complicadas quando você deseja misturar lógica reativa com lógica não reativa.
Por exemplo, imagine que você quer mostrar uma notificação quando o usuário se conecta ao chat. Você lê o tema atual (claro ou escuro) das props para que possa mostrar a notificação na cor correta:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Conectado!', theme);
});
connection.connect();
// ...
No entanto, theme
é um valor reativo (pode mudar como resultado de uma nova renderização), e cada valor reativo lido por um Efeito deve ser declarado como sua dependência. Agora você precisa especificar theme
como uma dependência do seu Efeito:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Conectado!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ Todas as dependências declaradas
// ...
Brinque com este exemplo e veja se consegue identificar o problema com essa experiência do usuário:
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { showNotification('Conectado!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); return <h1>Bem-vindo à sala {roomId}!</h1> } export default function App() { const [roomId, setRoomId] = useState('geral'); const [isDark, setIsDark] = useState(false); return ( <> <label> Escolha a sala de chat:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="geral">geral</option> <option value="viagem">viagem</option> <option value="musica">música</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Usar tema escuro </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Quando o roomId
muda, o chat reconecta como você esperaria. Mas como theme
também é uma dependência, o chat também reconecta sempre que você alterna entre o tema escuro e o claro. Isso não é legal!
Em outras palavras, você não quer que esta linha seja reativa, mesmo que esteja dentro de um Efeito (que é reativo):
// ...
showNotification('Conectado!', theme);
// ...
Você precisa de uma maneira de separar essa lógica não reativa da reativa ao seu redor.
Declarando um Evento de Efeito
Use um Hook especial chamado useEffectEvent
para extrair essa lógica não reativa dos seus Efeitos:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Conectado!', theme);
});
// ...
Aqui, onConnected
é chamado de um Evento de Efeito. É uma parte da lógica do seu Efeito, mas se comporta muito mais como um manipulador de eventos. A lógica dentro dele não é reativa, e sempre “vê” os valores mais recentes das suas props e estado.
Agora você pode chamar o Evento de Efeito onConnected
de dentro do seu Efeito:
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Conectado!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...
Isso resolve o problema. Note que você teve que remover onConnected
da lista de dependências do seu Efeito. Eventos de Efeito não são reativos e devem ser omitidos das dependências.
Verifique se o novo comportamento funciona como você esperaria:
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Conectado!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Bem-vindo à sala {roomId}!</h1> } export default function App() { const [roomId, setRoomId] = useState('geral'); const [isDark, setIsDark] = useState(false); return ( <> <label> Escolha a sala de chat:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="geral">geral</option> <option value="viagem">viagem</option> <option value="musica">música</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Usar tema escuro </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Você pode pensar nos Eventos de Efeito como sendo muito semelhantes aos manipuladores de eventos. A principal diferença é que os manipuladores de eventos são executados em resposta a interações do usuário, enquanto os Eventos de Efeito são acionados por você a partir dos Efeitos. Os Eventos de Efeito permitem que você “quebre a cadeia” entre a reatividade dos Efeitos e o código que não deve ser reativo.
Lendo as últimas props e estado com Eventos de Efeito
Os Eventos de Efeito permitem que você conserte muitos padrões nos quais você pode estar tentado a suprimir o linter de dependência.
Por exemplo, digamos que você tem um Efeito para registrar as visitas à página:
function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}
Depois, você adiciona várias rotas ao seu site. Agora seu componente Page
recebe uma prop url
com o caminho atual. Você quer passar a url
como parte do seu chamado de logVisit
, mas o linter de dependências reclama:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect tem uma dependência ausente: 'url'
// ...
}
Pense sobre o que você quer que o código faça. Você quer registrar uma visita separada para diferentes URLs, pois cada URL representa uma página diferente. Em outras palavras, essa chamada de logVisit
deve ser reativa em relação à url
. É por isso que, nesse caso, faz sentido seguir o linter de dependências e adicionar url
como uma dependência:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ Todas as dependências declaradas
// ...
}
Agora digamos que você quer incluir o número de itens no carrinho de compras junto com cada visita à página:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect tem uma dependência ausente: 'numberOfItems'
// ...
}
Você usou numberOfItems
dentro do Efeito, então o linter pede que você a adicione como uma dependência. No entanto, você não quer que a chamada de logVisit
seja reativa em relação a numberOfItems
. Se o usuário coloca algo no carrinho de compras, e numberOfItems
muda, isso não significa que o usuário visitou a página novamente. Em outras palavras, visitar a página é, em certo sentido, um “evento”. Acontece em um momento preciso no tempo.
Divida o código em duas partes:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ Todas as dependências declaradas
// ...
}
Aqui, onVisit
é um Evento de Efeito. O código dentro dele não é reativo. É por isso que você pode usar numberOfItems
(ou qualquer outro valor reativo!) sem se preocupar que isso fará com que o código circundante seja reexecutado.
Por outro lado, o Efeito em si permanece reativo. O código dentro do Efeito usa a prop url
, então o Efeito será executado após cada nova renderização com uma url
diferente. Isso, por sua vez, chamará o Evento de Efeito onVisit
.
Como resultado, você chamará logVisit
para cada mudança na url
, e sempre lerá a numberOfItems
mais recente. No entanto, se numberOfItems
mudar por conta própria, isso não fará com que nenhum dos códigos seja reexecutado.
Deep Dive
Nos bases de código existentes, você pode às vezes ver a regra de lint suprimida assim:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 Evite suprimir o linter assim:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}
Depois que useEffectEvent
se tornar uma parte estável do React, recomendamos nunca suprimir o linter.
O primeiro problema de suprimir a regra é que o React não alertará mais você quando seu Efeito precisa “reagir” a uma nova dependência reativa que você introduziu em seu código. No exemplo anterior, você adicionou url
às dependências porque o React lembrou você de fazer isso. Você não receberá mais tais lembretes para futuras edições desse Efeito se desativar o linter. Isso leva a bugs.
Aqui está um exemplo de um bug confuso causado pela supressão do linter. Neste exemplo, a função handleMove
deve ler o valor atual da variável de estado canMove
para decidir se o ponto deve seguir o cursor. No entanto, canMove
sempre é true
dentro de handleMove
.
Você consegue ver por quê?
import { useState, useEffect } from 'react'; export default function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [canMove, setCanMove] = useState(true); function handleMove(e) { if (canMove) { setPosition({ x: e.clientX, y: e.clientY }); } } useEffect(() => { window.addEventListener('pointermove', handleMove); return () => window.removeEventListener('pointermove', handleMove); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> <label> <input type="checkbox" checked={canMove} onChange={e => setCanMove(e.target.checked)} /> O ponto pode se mover </label> <hr /> <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity: 0.6, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> </> ); }
O problema com esse código está em suprimir o linter de dependências. Se você remover a supressão, verá que este Efeito deve depender da função handleMove
. Isso faz sentido: handleMove
é declarada dentro do corpo do componente, o que a torna um valor reativo. Todo valor reativo deve ser especificado como uma dependência, ou poderá potencialmente ficar obsoleto com o tempo!
O autor do código original “mentiu” para o React ao dizer que o Efeito não depende ([]
) de nenhum valor reativo. É por isso que o React não re-sincronizou o Efeito após canMove
ter mudado (e handleMove
com ele). Como o React não re-sincronizou o Efeito, o handleMove
anexado como ouvinte é a função handleMove
criada durante a renderização inicial. Durante a renderização inicial, canMove
era true
, e é por isso que handleMove
da renderização inicial sempre verá esse valor.
Se você nunca suprimir o linter, você nunca verá problemas com valores obsoletos.
Com useEffectEvent
, não há necessidade de “mentir” para o linter, e o código funciona como você esperaria:
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [canMove, setCanMove] = useState(true); const onMove = useEffectEvent(e => { if (canMove) { setPosition({ x: e.clientX, y: e.clientY }); } }); useEffect(() => { window.addEventListener('pointermove', onMove); return () => window.removeEventListener('pointermove', onMove); }, []); return ( <> <label> <input type="checkbox" checked={canMove} onChange={e => setCanMove(e.target.checked)} /> O ponto pode se mover </label> <hr /> <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity: 0.6, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> </> ); }
Isso não significa que useEffectEvent
é sempre a solução correta. Você deve aplicá-lo apenas nas linhas de código que você não quer que sejam reativas. No sandbox acima, você não queria que o código do Efeito fosse reativo em relação a canMove
. É por isso que fez sentido extrair um Evento de Efeito.
Leia Removendo Dependências de Efeito para outras alternativas corretas à supressão do linter.
Limitações dos Eventos de Efeito
Os Eventos de Efeito são muito limitados em como você pode usá-los:
- Chame-os apenas de dentro de Efeitos.
- Nunca os passe para outros componentes ou Hooks.
Por exemplo, não declare e passe um Evento de Efeito assim:
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000); // 🔴 Evite: Passando Eventos de Efeito
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Necessita especificar "callback" nas dependências
}
Em vez disso, sempre declare Eventos de Efeito diretamente ao lado dos Efeitos que os usam:
function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Bom: Chamado apenas localmente dentro de um Efeito
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // Não é necessário especificar "onTick" (um Evento de Efeito) como uma dependência
}
Os Eventos de Efeito são “partes” não reativas do seu código de Efeito. Eles devem estar ao lado do Efeito que os usa.
Recap
- Manipuladores de eventos são executados em resposta a interações específicas.
- Efeitos são executados sempre que a sincronização é necessária.
- A lógica dentro dos manipuladores de eventos não é reativa.
- A lógica dentro dos Efeitos é reativa.
- Você pode mover a lógica não reativa dos Efeitos para Eventos de Efeito.
- Chame Eventos de Efeito apenas de dentro de Efeitos.
- Não passe Eventos de Efeito para outros componentes ou Hooks.
Challenge 1 of 3: Corrija uma variável que não se atualiza
Este componente Timer
mantém uma variável de estado count
que aumenta a cada segundo. O valor pelo qual está aumentando é armazenado na variável de estado increment
. Você pode controlar a variável increment
com os botões de mais e menos.
No entanto, não importa quantas vezes você clicar no botão de mais, o contador ainda é incrementado em um a cada segundo. O que há de errado com este código? Por que increment
sempre é igual a 1
dentro do código do Efeito? Encontre o erro e corrija-o.
import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); useEffect(() => { const id = setInterval(() => { setCount(c => c + increment); }, 1000); return () => { clearInterval(id); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> <h1> Contador: {count} <button onClick={() => setCount(0)}>Reiniciar</button> </h1> <hr /> <p> A cada segundo, incrementar em: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }