Separating Events from Effects
Manipuladores de eventos só são executados novamente quando você realiza a mesma interação outra vez. Diferente dos manipuladores de eventos, os Effects se resincronizam 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 Effect que é executado novamente 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 Effect
- Por que os Effects 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 Effect não seja reativa
- O que são Eventos de Effect e como extraí-los dos seus Effects
- Como ler as últimas props e estado dos Effects usando Eventos de Effect
Escolhendo entre manipuladores de eventos e Effects
Primeiro, vamos recapitular a diferença entre manipuladores de eventos e Effects.
Imagine que você está implementando um componente de sala 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 dizer 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 Effects? Toda vez que você 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 em qualquer outro momento ou por qualquer outro motivo. É 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}>Send</button>
</>
);
}Com um manipulador de eventos, você pode ter certeza de que sendMessage(message) só será executado se o usuário pressionar o botão.
Effects são executados sempre que a sincronização é necessária
Lembre-se que você também precisa manter o componente conectado à sala de chat. Onde esse código vai?
O motivo para executar este código não é alguma interação específica. Não importa por que ou como o usuário navegou para a tela da sala de chat. Agora que eles estão olhando para ela e podem 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 Effect:
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 chat atualmente selecionado, independentemente das interações específicas realizadas pelo usuário. Se o usuário apenas abriu seu aplicativo, selecionou uma sala diferente ou navegou para outra tela e voltou, seu Effect garantirá que o componente permaneça sincronizado com a sala selecionada no momento, e se reconectará 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 os manipuladores de eventos são sempre acionados “manualmente”, por exemplo, clicando em um botão. Os Effects, por outro lado, são “automáticos”: eles são executados e reexecutados quantas vezes for necessário para permanecerem sincronizados.
Existe uma maneira mais precisa de pensar sobre isso.
Props, estado e variáveis declaradas dentro do corpo do seu componente são chamados 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 re-renderização. Por exemplo, o usuário pode editar a message ou escolher um roomId diferente em um menu suspenso. Manipuladores de eventos e Effects respondem às mudanças de maneiras diferentes:
- 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. Manipuladores de eventos podem ler valores reativos sem “reagir” às suas mudanças.
- A lógica dentro dos Effects é reativa. Se o seu Effect lê um valor reativo, você tem que especificá-lo como uma dependência. Então, se uma re-renderização fizer com que esse valor mude, o React executará novamente a lógica do seu Effect com o novo valor.
Vamos revisitar o exemplo anterior para ilustrar essa diferença.
A lógica dentro dos manipuladores de eventos não é reativa
Dê uma olhada nesta 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 ela pertence ao manipulador de eventos:
function handleSendClick() {
sendMessage(message);
}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 Effects é reativa
Agora vamos voltar a estas 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 esse valor for diferente. É por isso que pertence a um Effect:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);Os Effects são reativos, então createConnection(serverUrl, roomId) e connection.connect() serão executados para cada valor distinto de roomId. Seu Effect mantém a conexão de chat sincronizada com a sala atualmente selecionada.
Extraindo lógica não reativa de Effects
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 chat. Você lê o tema atual (escuro ou claro) das props para poder 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();
// ...No entanto, theme é um valor reativo (ele pode mudar como resultado de uma re-renderização), e todo valor reativo lido por um Effect deve ser declarado como sua dependência. Agora você tem que especificar theme como uma dependência do seu Effect:
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 consegue identificar o problema com a 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 chat reconecta como você esperaria. Mas como theme também é uma dependência, o chat também reconecta toda vez que você alterna entre o tema escuro e o claro. Isso não é bom!
Em outras palavras, você não quer que esta linha seja reativa, mesmo que esteja dentro de um Effect (que é reativo):
// ...
showNotification('Connected!', theme);
// ...Você precisa de uma maneira de separar essa lógica não reativa da lógica reativa do Effect ao redor dela.
Declarando um Evento de Effect
Use um Hook especial chamado useEffectEvent para extrair essa lógica não reativa do seu Effect:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...Aqui, onConnected é chamado de Evento de Effect. É uma parte da lógica do seu Effect, mas se comporta muito mais como um manipulador de eventos. A lógica dentro dele não é reativa e ele sempre “vê” os valores mais recentes de suas props e estado.
Agora você pode chamar o Evento de Effect onConnected de dentro do seu Effect:
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 theme da lista de dependências do seu Effect, pois ele não é mais usado no Effect. Você também não precisa adicionar onConnected a ele, porque Eventos de Effect 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 { 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 em Eventos de Effect como sendo muito semelhantes a 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 Effect são acionados por você a partir de Effects. Eventos de Effect permitem que você “quebre a corrente” entre a reatividade dos Effects e o código que não deveria ser reativo.
Lendo os props e o estado mais recentes com Eventos de Effect
Eventos de Effect permitem corrigir muitos padrões onde você poderia ser tentado a suprimir o linter de dependências.
Por exemplo, digamos que você tenha um Effect para registrar as visitas à página:
function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}Mais tarde, 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 da sua chamada logVisit, mas o linter de dependências reclama:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 O Hook useEffect do React tem uma dependência faltando: '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 deve ser reativa em relação à url. É por isso que, neste 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ê queira 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]); // 🔴 O Hook useEffect do React tem uma dependência faltando: 'numberOfItems'
// ...
}Você usou numberOfItems dentro do Effect, então o linter pede para você adicioná-lo como uma dependência. No entanto, você não quer que a chamada logVisit seja reativa em relação a numberOfItems. Se o usuário colocar algo no carrinho de compras e numberOfItems mudar, 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 Effect. 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 em caso de mudanças.
Por outro lado, o próprio Effect permanece reativo. O código dentro do Effect usa a prop url, então o Effect será reexecutado após cada re-renderização com uma url diferente. Isso, por sua vez, chamará o Evento de Effect onVisit.
Como resultado, você chamará logVisit para cada mudança na url e sempre lerá o numberOfItems mais recente. No entanto, se numberOfItems mudar por si só, isso não fará com que nenhum código seja reexecutado.
Deep Dive
Nos códigos 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]);
// ...
}Recomendamos nunca suprimir o linter.
A primeira desvantagem de suprimir a regra é que o React não o alertará mais quando seu Effect precisar “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 o lembrou de fazer isso. Você não receberá mais tais lembretes para edições futuras desse Effect 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 este código está na supressão do linter de dependências. Se você remover a supressão, verá que este Effect deveria 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 ele pode ficar obsoleto com o tempo!
O autor do código original “mentiu” para o React, dizendo que o Effect não depende ([]) de nenhum valor reativo. É por isso que o React não resincronizou o Effect após canMove ter mudado (e handleMove com ele). Como o React não resincronizou o Effect, 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á esse valor.
Se você nunca suprimir o linter, 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 { 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 seja sempre a solução correta. Você só deve aplicá-lo às linhas de código que você não quer que sejam reativas. Na 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 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:
- Chame-os apenas de dentro de Effects.
- Nunca os passe para outros componentes ou Hooks.
Por exemplo, não declare e passe um Evento de Effect como este:
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000); // 🔴 Evite: Passar 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 Eventos de Effect diretamente ao lado dos Effects que os utilizam:
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 é necessário especificar "onTick" (um Evento de Effect) como dependência
}Eventos de Effect são “pedaços” não reativos do seu código de Effect. Eles devem estar ao lado do Effect que os utiliza.
Recap
- 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 de manipuladores de eventos não é reativa.
- A lógica dentro de Effects é reativa.
- Você pode mover a lógica não reativa de Effects para Eventos de Effect.
- Chame Eventos de Effect apenas de dentro de Effects.
- Não passe Eventos de Effect para outros componentes ou Hooks.
Challenge 1 of 4: Corrija 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 ele 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 é incrementado em um a cada segundo. O que está 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> </> ); }