Separando Eventos de Efeitos
Manipuladores de eventos só são executados novamente quando você realiza a mesma interação novamente. Diferente dos manipuladores de eventos, os Efeitos re-sincronizam 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 dos dois comportamentos: um Efeito que roda novamente em resposta a alguns valores, mas não a outros. Esta página vai te 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 sã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 de seus Efeitos
- Como ler as últimas props e estado de Efeitos utilizando Eventos de Efeito
Escolhendo entre manipuladores de eventos e Efeitos
Primeiramente, vamos recapitular a diferença entre manipuladores de eventos e Efeitos.
Imagine que você está implementando um componente de sala de bate-papo. Seus requisitos são estes:
- Seu componente deve conectar-se automaticamente à sala de bate-papo selecionada.
- Quando você clica no botão “Enviar”, ele deve enviar uma mensagem para o bate-papo.
Digamos que você já implementou o código para eles, mas você não tem certeza de onde colocá-lo. Você deveria usar manipuladores de eventos ou Efeitos? Toda vez que você precisar responder a esta pergunta, considere por que o código precisa rodar.
Manipuladores de eventos rodam em resposta a interações específicas
Da perspectiva do usuário, enviar uma mensagem deve acontecer porque o botão “Enviar” em particular foi clicado. O usuário ficará bem chateado se você enviar a mensagem em qualquer outro momento ou por qualquer outra razão. É por isso que enviar uma mensagem deve ser um manipulador de eventos. Manipuladores de eventos permitem que você lide com 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)
vai somente rodar se o usuário pressionar o botão.
Efeitos rodam sempre que a sincronização é necessária
Lembre-se de que você também precisa manter o componente conectado à sala de bate-papo. Onde esse código vai?
A razão para rodar este código não é alguma interação em particular. Não importa por que ou como o usuário navegou até a tela da sala de bate-papo. Agora que ele está olhando para ela e poderia interagir com ela, o componente precisa ficar conectado ao servidor de bate-papo selecionado. Mesmo se 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 conectar. É por isso que é um Efeito:
function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
Com este código, você pode ter certeza de que sempre há uma conexão ativa com o servidor de bate-papo atualmente selecionado, independentemente das interações específicas executadas pelo usuário. Seja o usuário apenas abrindo seu aplicativo, selecionando uma sala diferente ou navegando para outra tela e voltando, seu Efeito garante que o componente permaneça sincronizado com a sala atualmente selecionada, e re-conectará sempre que for 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>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> <button onClick={handleSendClick}>Send</button> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Valores reativos e lógica reativa
Intuitivamente, você poderia dizer que manipuladores de eventos sempre são acionados “manualmente”, por exemplo, clicando em um botão. Efeitos, por outro lado, são “automáticos”: eles rodam e rodam novamente com a frequência necessária para permanecerem sincronizados.
Há uma maneira mais precisa de pensar sobre isto.
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 no fluxo de dados de renderização:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}
Valores reativos como estes podem mudar devido à uma nova renderização. Por exemplo, o usuário pode editar o message
ou escolher um roomId
diferente em um dropdown. Manipuladores de eventos e Efeitos respondem às mudanças de forma diferente:
- A lógica dentro dos manipuladores de eventos não é reativa. Ela não rodará novamente a menos que o usuário execute a mesma interação (por exemplo, um clique) novamente. 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 especificar ele como uma dependência. Então, se uma nova renderização fizer com que esse valor mude, o React irá rodar novamente a lógica do seu Efeito com o novo valor.
Vamos revisitar o exemplo anterior para ilustrar esta diferença.
Lógica dentro dos manipuladores de eventos não é reativa
Dê uma olhada nesta linha de código. Essa lógica deveria ser reativa ou não?
// ...
sendMessage(message);
// ...
Da perspectiva do usuário, uma mudança para o message
não significa que ele quer enviar uma mensagem. Significa apenas 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 rodar novamente só porque o valor reativo mudou. É por isso que ela pertence no manipulador de eventos:
function handleSendClick() {
sendMessage(message);
}
Manipuladores de eventos não são reativos, então sendMessage(message)
só rodará quando o usuário clicar no botão Enviar.
Lógica dentro dos Efeitos é reativa
Agora vamos retornar a estas linhas:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...
Da perspectiva do usuário, uma mudança para o roomId
significa que ele quer se conectar a uma sala diferente. Em outras palavras, a lógica para se conectar à sala deve ser reativa. Você quer que estas linhas de código “acompanhem” o valor reativo, e que rodem novamente se esse valor for diferente. É por isso que ele pertence a um Efeito:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
Efeitos são reativos, então createConnection(serverUrl, roomId)
e connection.connect()
rodarão para cada valor distinto de roomId
. Seu Efeito mantém a conexão do bate-papo sincronizada com a sala atualmente selecionada.
Extraindo lógica não reativa de Efeitos
As coisas ficam mais complicadas quando você quer 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 bate-papo. Você lê o tema atual (escuro ou claro) das props para que você possa mostrar a notificação na cor correta:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...
Entretanto, theme
é um valor reativo (ele pode mudar como resultado de uma nova renderização), e todo valor reativo lido por um Efeito deve ser declarado como sua dependência. Agora você tem que especificar theme
como uma dependência do seu Efeito:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ Todas as dependências declaradas
// ...
Brinque com este exemplo e veja se você consegue identificar o problema com esta 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('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Quando o roomId
muda, o bate-papo re-conecta como você esperaria. Mas como theme
também é uma dependência, o bate-papo também re-conecta toda vez que você alterna entre o tema escuro e o tema claro. Isso não é bom!
Em outras palavras, você não quer que esta linha seja reativa, mesmo que ela esteja dentro de um Efeito (que é reativo):
// ...
showNotification('Connected!', theme);
// ...
Você precisa de um modo de separar esta lógica não reativa do Efeito reativo ao redor dela.
Declarando um Evento de Efeito
Use um Hook especial chamado useEffectEvent
para extrair essa lógica não reativa de seu Efeito:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...
Aqui, onConnected
é chamado de um Evento de Efeito. É uma parte da lógica do seu Efeito, mas ela se comporta muito mais como um manipulador de eventos. A lógica dentro dela não é reativa, e ela sempre “vê” os últimos valores de 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('Connected!', 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('Connected!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Você pode pensar nos Eventos de Efeito como sendo muito similares aos manipuladores de eventos. A principal diferença é que os manipuladores de eventos rodam em resposta a interações do usuário, enquanto os Eventos de Efeito são acionados por você de Efeitos. Eventos de Efeito permitem que você “quebre a cadeia” entre a reatividade dos Efeitos e o código que não deveria ser reativo.
Lendo as últimas props e estado com Eventos de Efeito
Eventos de Efeito permitem que você conserte muitos padrões onde você pode ser tentado a suprimir o linter de dependência.
Por exemplo, diga que você tem um Efeito para registrar as visitas da página:
function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}
Mais tarde, você adiciona múltiplas rotas ao seu site. Agora seu componente Page
recebe uma prop url
com o caminho atual. Você quer passar a url
como parte de sua chamada logVisit
, mas o linter de dependência reclama:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url'
// ...
}
Pense sobre o que você quer que o código faça. Você quer registrar uma visita separada para URLs diferentes, já que cada URL representa uma página diferente. Em outras palavras, esta chamada logVisit
deveria ser reativa em relação à url
. É por isso que, neste caso, faz sentido seguir o linter de dependência, e adicionar url
como uma dependência:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ Todas as dependências declaradas
// ...
}
Agora, digamos que você quer inlcuir o número de itens no carrinho de compras juntamente com cada visita à página:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}
Você usou numberOfItems
dentro do Efeito, então o linter pede que você adicione ele como uma dependência. Entretanto, você não quer que a chamada logVisit
seja reativa em relação à numberOfItems
. Se o usuário coloca algo no carrinho de compras, e o 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”. Ele 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 ao redor re-execute nas mudanças.
Por outro lado, o próprio Efeito permanece reativo. Código dentro do Efeito usa a prop url
, então o Efeito irá rodar novamente após cada nova renderização com um url
diferente. Isso, por sua vez, irá chamar o Evento de Efeito onVisit
.
Como resultado, você irá chamar logVisit
para cada mudança para a url
, e sempre ler a última numberOfItems
. Entretanto, se numberOfItems
mudar por conta própria, isso não fará com que nenhum dos códigos seja executado novamente.
Deep Dive
Nos códigos base existentes, você pode às vezes ver a regra de lint suprimida desta maneira:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 Avoid suppressing the linter like this:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}
Depois que useEffectEvent
se tornar uma parte estável do React, nós recomendamos nunca suprimir o linter.
A primeira desvantagem de suprimir a regra é que o React não irá mais avisá-lo quando seu Efeito precisar “reagir” a uma nova dependência reativa que você introduziu ao seu código. No exemplo anterior, você adicionou url
às dependências porque o React te lembrou de fazê-lo. Você não irá mais receber tais lembretes para quaisquer edições futuras para aquele Efeito se você desabilitar o linter. Isso leva a erros (bugs).
Aqui está um exemplo de um bug confuso causado pela supressão do linter. Neste exemplo, a função handleMove
é suposta ler o valor atual da variável de estado canMove
para decidir se o ponto deveria seguir o cursor. Entretanto, 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)} /> The dot is allowed to move </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 este código está em suprimir o linter de dependência. Se você remover a supressão, você verá que este Efeito deveria depender da função handleMove
. Isso faz sentido: handleMove
é declarada dentro do corpo do componente, o que a torna um valor reativo. Cada valor reativo deve ser especificado como uma dependência, ou ele pode potencialmente ficar obsoleto com o tempo!
O autor do código original “mentiu” para o React dizendo que o Efeito não depende ([]
) de nenhum valor reativo. É por isso que o React não re-sincronizou o Efeito depois que canMove
mudou (e handleMove
com ele). Como o React não re-sincronizou o Efeito, o handleMove
anexado como um listener é a função handleMove
criada durante a renderização inicial. Durante a renderização inicial, canMove
era true
, que é por que handleMove
da renderização inicial sempre verá aquele 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)} /> The dot is allowed to move </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ê só deve aplicá-lo às linhas de código que não deseja que sejam reativas. No sandbox acima, você não queria que o código do Effect fosse reativo em relação a canMove
. É por isso que fez sentido extrair um Evento de Effect.
Leia Removendo as Dependências de Effect para outras alternativas corretas para suprimir o linter.
Limitações dos Eventos de Effect
Os Eventos de Effect são muito limitados em como você pode usá-los:
- Apenas chame-os de dentro de Effects.
- Nunca os passe para outros componentes ou Hooks.
Por exemplo, não declare e passe um Evento de Effect assim:
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000); // 🔴 Evitar: Passando Eventos de Effect
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Precisa especificar "callback" nas dependências
}
Em vez disso, sempre declare os Eventos de Effect diretamente próximos aos Effects 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 Effect
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // Não há necessidade de especificar "onTick" (um Evento de Effect) como uma dependência
}
Os Eventos de Effect são “pedaços” não reativos do código do seu Effect. Eles devem estar próximos ao Effect que os usa.
Recap
- Os manipuladores de eventos são executados em resposta a interações específicas.
- Effects 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 Effects é reativa.
- Você pode mover a lógica não reativa dos Effects para os Eventos de Effect.
- Apenas chame os Eventos de Effect de dentro de Effects.
- Não passe os Eventos de Effect para outros componentes ou Hooks.
Challenge 1 of 4: Consertar uma variável que não atualiza
Este componente Timer
mantém uma variável de estado count
que aumenta a cada segundo. O valor pelo qual ela 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ê clique no botão de mais, o contador ainda incrementa 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 Effect? 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> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }