Eventos no React: onClick, onChange e o objeto do evento
Como ligar funções a eventos no JSX, por que onClick recebe a função e não a chamada, passar argumento e o que o evento sintético faz.
Você abre o sistema da biblioteca e, antes de clicar, a tela já registra uma
reserva. Em outra tentativa, o botão aparece, mas não faz nada. Os dois sintomas
podem nascer do mesmo detalhe: um par de parênteses colocado no onClick.
Um evento é uma ação percebida pela interface — a tela que a pessoa usa —, como clique, digitação ou envio. No React, você conecta uma função, bloco de instruções reutilizável, a essa ação por uma prop, valor entregue ao componente. A função que responde recebe o nome de event handler, ou simplesmente handler.
Os exemplos seguem uma biblioteca de bairro: reserva, busca e carteirinha. Todos rodaram no Chromium 151.0.7922.34 com React 19.2.8, e as saídas são do console, painel do navegador que mostra mensagens e erros do código.
Qual é a diferença entre onClick={reservar} e onClick={reservar()}?
onClick={reservar} entrega a função para React chamá-la no clique.
onClick={reservar()} executa a função durante a renderização — processo de
produzir a tela — e entrega ao evento apenas o valor retornado.
Se a bibliotecária pede “me passe o telefone da Ana”, você entrega o número;
você não liga para a Ana naquele instante e entrega o que ela respondeu. No
JSX — sintaxe parecida com HTML, a marcação da página, dentro do JavaScript
—, a referência
reservar é o número guardado para depois. A chamada
reservar() é a ligação acontecendo agora. O React precisa da primeira forma
para decidir quando executar o handler.
O limite da analogia é que um telefone é só um dado, enquanto uma referência de função aponta para código executável. A comparação serve para lembrar quando a ação acontece, não para explicar o funcionamento interno do React.
Experimente você mesmo
Antes de montar o primeiro componente, marque em qual momento cada mensagem deveria aparecer: renderização ou clique. Depois abra o console, recarregue a página sem clicar e confira. Se já houver saída, a função foi chamada; se ela esperar o botão, a função foi passada.
function reservar() {
console.log('reserva registrada');
}
function Estante() {
console.log('--- Estante renderizou ---');
return (
<ul>
<li>
<button onClick={reservar}>Reservar Dom Casmurro</button>
</li>
<li>
<button onClick={reservar()}>Reservar Vidas Secas</button>
</li>
</ul>
);
}Cliquei uma vez em cada botão, nessa ordem:
Leia de cima para baixo. O reserva registrada apareceu antes de qualquer
clique, na linha logo depois de “Estante renderizou”: foi o reservar() do
segundo botão sendo executado durante a renderização. E o clique no segundo
botão não imprimiu nada, porque o que ficou guardado na prop onClick foi o
retorno de reservar, que é undefined.
O motivo é o JSX: { } no JSX é uma expressão
JavaScript comum, avaliada na hora em que o componente roda. reservar é uma
referência à função; reservar() é o resultado da chamada.
O retorno é undefined, ausência de valor. Uma API é um serviço com o qual a
aplicação troca dados; setState pede atualização de estado, dado que o
componente lembra. Se isso dispara durante a renderização, pode criar um loop
infinito — repetição que não termina.
Como passar argumentos no onClick sem executar a função antes?
Entregue ao onClick uma arrow function que, no clique, chama o handler com os
argumentos desejados. Arrow function é a forma curta de criar uma função
usando =>.
É como anexar ao telefone um bilhete: “quando ligarem, informe o código deste livro”. Cada botão guarda uma instrução própria sem fazer a ligação agora.
acervo é um array, uma lista; map percorre seus livros; e key identifica
cada item para React. No clique, React envia um objeto com detalhes do evento;
currentTarget aponta o elemento cujo handler está rodando.
const acervo = [
{ id: 'DC-14', titulo: 'Dom Casmurro' },
{ id: 'VS-07', titulo: 'Vidas Secas' },
];
function Estante() {
function reservar(id, evento) {
console.log(`reservando ${id} a partir de <${evento.currentTarget.tagName}>`);
}
return (
<ul>
{acervo.map((livro) => (
<li key={livro.id}>
<button onClick={(evento) => reservar(livro.id, evento)}>
Reservar {livro.titulo}
</button>
</li>
))}
</ul>
);
}Experimente você mesmo
Antes de olhar a saída, preveja: ao clicar em “Vidas Secas”, qual id será
entregue a reservar e qual nome de tag aparecerá em currentTarget? Siga o
objeto livro e a arrow do botão antes de conferir.
Cliquei em “Vidas Secas” e depois em “Dom Casmurro”:
A arrow fica guardada e, no clique, repassa evento e livro.id. A saída confirma
o código do livro e o BUTTON responsável pelo handler.
Se você não precisa do evento, pode omitir: onClick={() => reservar(livro.id)}.
E vale lembrar que essa arrow nasce dentro do map — o mesmo map que exige a
prop key, assunto de renderizar listas no React.
Até aqui, você já sabe: para passar argumentos sem antecipar a execução, crie uma função que espere o evento e faça a chamada no momento certo.
O que existe no objeto de evento do React?
O handler recebe um evento sintético, objeto criado por React com informações
como tipo, origem e elemento responsável pelo handler. O evento original do
navegador continua acessível em nativeEvent.
O exemplo imprime essas propriedades e o nome do construtor — estrutura usada
para criar o objeto. O texto fica num span para separar origem e botão:
function BotaoReserva() {
function inspecionar(evento) {
console.log('constructor.name :', evento.constructor.name);
console.log('type :', evento.type);
console.log('target :', evento.target.tagName, `"${evento.target.textContent}"`);
console.log('currentTarget :', evento.currentTarget.tagName);
console.log('bubbles :', evento.bubbles);
console.log('nativeEvent :', evento.nativeEvent.constructor.name);
console.log('é o mesmo type? :', evento.type === evento.nativeEvent.type);
console.log('isTrusted :', evento.nativeEvent.isTrusted);
}
return (
<button onClick={inspecionar}>
Reservar <span>Dom Casmurro</span>
</button>
);
}Clicando em cima da palavra “Dom Casmurro”:
O construtor SyntheticBaseEvent confirma o evento sintético, que apresenta ao
código uma interface consistente entre navegadores.
A segunda: nativeEvent é um PointerEvent, não um MouseEvent. O navegador
moderno dispara clique como evento de ponteiro — mouse, dedo e caneta pela mesma
porta. Quando alguém precisa de algo que só existe no evento cru, é por
evento.nativeEvent que se chega lá.
A terceira, e a mais útil no dia a dia: target é o SPAN e currentTarget é
o BUTTON. São coisas diferentes.
| propriedade | o que é | quando você usa |
|---|---|---|
target |
o elemento mais fundo onde o evento nasceu | descobrir em qual filho a pessoa clicou |
currentTarget |
o elemento que tem o handler que está rodando agora | ler value, dataset ou href do próprio elemento |
nativeEvent |
o evento cru do navegador | PointerEvent, coordenadas, propriedades fora da interface do React |
type |
o nome do evento, em minúsculas | um handler só que atende click e keydown |
dataset reúne atributos personalizados escritos como data-*; href guarda
o endereço de um link.
Errar entre target e currentTarget é o bug clássico de card clicável: você lê
evento.target.dataset.id esperando o card e recebe o <span> do título.
Como evitar que um formulário React recarregue a página?
onSubmit é a prop que responde ao envio do formulário. No início do handler,
chame evento.preventDefault() para cancelar o envio do navegador sem impedir a
validação ou o uso dos dados.
Formulário envia e navega por padrão; link também navega. React não remove esses comportamentos da plataforma.
function Cadastro() {
function inscrever(evento) {
console.log('handler do React rodou');
}
return (
<form onSubmit={inscrever}>
<input name="leitor" defaultValue="Ana Prado" />
<button type="submit">Fazer carteirinha</button>
</form>
);
}Cliquei em “Fazer carteirinha”. A segunda linha é o registro de navegação da própria página:
O handler rodou, e logo depois o navegador enviou o formulário pela URL e recarregou tudo. A aplicação inteira reiniciou. É exatamente o sintoma que aparece como “meu formulário pisca e some o que eu digitei”.
Com preventDefault, o padrão é cancelado e a página fica de pé:
function Cadastro() {
function inscrever(evento) {
evento.preventDefault();
const dados = new FormData(evento.currentTarget);
console.log('handler do React rodou');
console.log('leitor:', dados.get('leitor'));
console.log('defaultPrevented:', evento.defaultPrevented);
}
return (
<form onSubmit={inscrever}>
<input name="leitor" defaultValue="Ana Prado" />
<button type="submit">Fazer carteirinha</button>
</form>
);
}Nenhuma navegação depois disso. E repare que evento.currentTarget aqui é o
próprio <form>. FormData é a ferramenta do navegador que lê pares de nome e
valor dos campos sem precisar de estado.
Quando você quiser que o React seja dono de cada tecla digitada, o caminho é o
formulário controlado.
Com link é a mesma mecânica, e o motivo mais comum é abrir o conteúdo dentro da própria aplicação em vez de trocar de página:
function Ficha() {
function abrirNoLeitor(evento) {
evento.preventDefault();
console.log(`abrindo no leitor interno: ${evento.currentTarget.getAttribute('href')}`);
}
return (
<p>
<a href="/acervo/dom-casmurro" onClick={abrirNoLeitor}>
Ler Dom Casmurro
</a>
</p>
);
}Tirando a linha do preventDefault, o mesmo clique produz isto — o navegador
foi embora para uma rota que não existe:
O 404 (Not Found) informa que a rota — caminho do endereço — não existe. O
handler rodou, mas sem preventDefault o navegador continuou a navegação.
Por que o clique no filho também dispara o handler do pai?
Porque o clique borbulha: depois de nascer no elemento mais interno, ele sobe pelos elementos que o envolvem e aciona seus handlers. Antes da subida, existe a fase de captura, que percorre o caminho da raiz até o alvo.
No React, onClick acompanha a bolha; onClickCapture, a descida. A árvore é a
hierarquia de elementos dentro de outros elementos.
Três níveis aninhados, com as duas fases em cada um:
function Estante() {
return (
<section
onClickCapture={() => console.log('1. captura — section.estante')}
onClick={() => console.log('6. bolha — section.estante')}
>
<article
onClickCapture={() => console.log('2. captura — article.ficha')}
onClick={() => console.log('5. bolha — article.ficha')}
>
<button
onClickCapture={() => console.log('3. captura — button')}
onClick={() => console.log('4. bolha — button')}
>
Reservar Dom Casmurro
</button>
</article>
</section>
);
}Um único clique no botão:
Seis handlers, um clique. Essa ordem é a mesma do DOM puro, descrita em objeto event, preventDefault e delegação.
Na tela de biblioteca, isso vira um problema concreto: a ficha do livro inteira é clicável para abrir o detalhe, e dentro dela existe um botão “cancelar reserva”. Clicar em cancelar abre o detalhe também.
function Ficha({ titulo, comStop }) {
function abrirDetalhe() {
console.log(`abriu a ficha de ${titulo}`);
}
function cancelarReserva(evento) {
if (comStop) evento.stopPropagation();
console.log(`cancelou a reserva de ${titulo}`);
}
return (
<article onClick={abrirDetalhe}>
<h2>{titulo}</h2>
<button onClick={cancelarReserva}>Cancelar reserva de {titulo}</button>
</article>
);
}stopPropagation interrompe a subida: o onClick do <article> não chega a ser
chamado. Não confunda com preventDefault, que não interrompe nada — apenas
cancela o comportamento nativo. São dois botões diferentes no mesmo painel.
Até aqui, você já sabe: preventDefault cancela uma ação do navegador;
stopPropagation impede o evento de continuar pela árvore. Um não substitui o
outro.
Como usar onChange, teclado e foco no React?
onChange acompanha cada alteração do campo; onKeyDown informa teclas;
onFocus marca a entrada do foco; e onBlur, a saída. Esses eventos permitem
responder enquanto a pessoa digita e navega pelo formulário.
Em um campo de texto, o change nativo normalmente espera o campo perder o
foco, enquanto onChange do React dispara a cada edição. No teste, ref cria
uma ligação com o input e useEffect roda após a montagem para registrar um
listener, função que escuta o evento:
import { useRef, useEffect } from 'react';
function Busca() {
const campo = useRef(null);
useEffect(() => {
campo.current.addEventListener('change', (e) =>
console.log(' [change nativo]', e.target.value)
);
}, []);
return (
<input
ref={campo}
placeholder="Buscar no acervo"
onChange={(evento) => console.log('[onChange React]', evento.target.value)}
/>
);
}Digitei graci e depois saí do campo com Tab:
Cinco disparos do React, um do navegador. Na prática o onChange do React se
comporta como o input nativo — e é por isso que a busca do acervo filtra
enquanto a pessoa digita, sem nenhum truque. Se você já usou
addEventListener em JavaScript puro, é essa
a tradução mental: onChange do React ≈ input do DOM.
Teclado e foco seguem o mesmo padrão de nome. onKeyDown recebe a tecla em
evento.key, e os modificadores em ctrlKey, shiftKey, altKey:
function BuscaAcervo() {
function aoTeclar(evento) {
console.log(`keydown: key=${evento.key} ctrl=${evento.ctrlKey} shift=${evento.shiftKey}`);
if (evento.key === 'Enter') {
console.log(` buscando por "${evento.currentTarget.value}"`);
}
if (evento.key === 'Escape') {
evento.currentTarget.value = '';
console.log(' campo limpo');
}
}
return (
<input
placeholder="Buscar no acervo"
onKeyDown={aoTeclar}
onFocus={() => console.log('foco: entrou no campo')}
onBlur={(evento) => console.log(`blur: saiu com "${evento.target.value}"`)}
/>
);
}Digitei Ra, apertei Enter, apertei Esc e saí com Tab:
Duas leituras dessa saída. O Tab aparece como keydown antes do blur: o
navegador ainda está no campo quando a tecla é processada. E o blur mostra
string vazia porque o Esc já tinha limpado o campo.
Onde React registra os listeners de eventos?
React registra a maioria dos listeners no contêiner raiz — elemento que abriga a aplicação — e dali distribui os eventos. Essa técnica se chama delegação de eventos; eventos que não borbulham exigem tratamento direto no elemento.
Por isso seu handler de onClick não é simplesmente anexado ao botão. O
diagrama acompanha o clique até a raiz e de volta à árvore React:
Para conferir, abra o DevTools, conjunto de ferramentas de desenvolvimento
do navegador. No console, getEventListeners lista listeners e $0 representa
o elemento selecionado na página:
getEventListeners($0);
Object.values(getEventListeners($('#root'))).flat().length;A segunda linha precisa do Object.values(...).flat() porque getEventListeners
devolve um objeto agrupado por tipo de evento, não um array: pedir .length nele
direto responde undefined.
Rodei esses comandos por dentro do protocolo do Chrome, numa página com dois
botões — um com onClick e outro sem nada:
O botão com onClick aparece com um listener de click — e o corpo dele é
function noop$1() {}, uma função vazia. É um remendo antigo do React para o
Safari do iPhone, que não borbulha clique em elemento não interativo. O React
prega esse onclick vazio em qualquer elemento que receba a prop onClick,
botão incluído, sem checar se aquela tag precisava do conserto. noop$1 foi
renomeado pelo empacotador, ferramenta que prepara arquivos para publicação.
De todo jeito, não é o seu handler: ele não faz nada.
O seu handler está do outro lado: 139 listeners no div#root, cobrindo 85
tipos de evento. Repare que não são duas fases para todo mundo — 53 tipos
aparecem nas duas e 32 aparecem só na captura. Esses 32 são justamente os
eventos que não borbulham (scroll, load, error, play, invalid e
companhia): não adianta esperar por eles subindo até a raiz, então para esses o
React desiste da delegação e põe o listener direto no elemento — um <video> com
onPlay carrega 24 listeners próprios de mídia.
Quando o clique sobe até lá, o React olha a própria árvore de componentes,
descobre o caminho do botão até a raiz e chama, na ordem, os onClickCapture e
os onClick que existem nesse caminho. Foi essa simulação que produziu a
sequência de 1 a 6 da seção anterior.
Isso tem uma consequência prática de ordem. Um listener nativo colocado em
document roda fora da simulação do React:
import { useEffect } from 'react';
// dentro do componente que tem o botão
useEffect(() => {
document.addEventListener('click', () => console.log('document — captura'), true);
document.addEventListener('click', () => console.log('document — bolha'));
}, []);A captura no document vem antes de tudo e a bolha vem depois de tudo, porque o
div#root está dentro do document. Se você mistura código React com script
antigo na mesma página, é aqui que a ordem inesperada nasce.
Até aqui, você já sabe: React delega a maior parte dos eventos à raiz, reconstrói o caminho na árvore e respeita a ordem entre captura e bolha.
Por que currentTarget vira null depois de um await?
Porque React preenche currentTarget apenas enquanto executa o handler e limpa
a propriedade ao terminar o disparo. Depois de await, esse momento já passou;
por isso o valor precisa ser copiado antes da espera.
Um handler assíncrono pode pausar com await enquanto aguarda uma
Promise, objeto que representa um resultado futuro. Essa pausa cria a
armadilha do próximo exemplo:
function BuscaAcervo() {
async function buscar(evento) {
await new Promise((r) => setTimeout(r, 50));
console.log('procurando por', evento.currentTarget.value);
}
return <input placeholder="Buscar no acervo" onChange={buscar} />;
}Basta digitar uma letra no campo:
O Uncaught (in promise) na frente conta metade da história: como o handler é
async, quem quebra é a promessa que ele devolveu, e o React não olha para ela.
Nada mais na tela para de funcionar — o erro só aparece no console, o que faz
esse bug sobreviver semanas em produção.
Nem tudo se perde, e a diferença importa na hora de depurar. Num botão, com
?. para não estourar o erro e conseguir imprimir os dois lados:
async function reservar(evento) {
console.log('antes do await :', evento.currentTarget?.tagName, evento.target.tagName);
await new Promise((r) => setTimeout(r, 50));
console.log('depois do await:', evento.currentTarget?.tagName, evento.target.tagName);
}O primeiro valor de cada linha é currentTarget, o segundo é target. Depois
do await, currentTarget é null — e o ?. transforma isso em undefined
na hora de imprimir. Já o target continua apontando para o elemento. É por
isso que a mensagem de erro fala em null sem citar o seu campo: o que ficou
vazio é a propriedade do evento do React, não o elemento do DOM.
A correção é ler o que você precisa antes de qualquer espera:
function BuscaAcervo() {
async function buscar(evento) {
const termo = evento.currentTarget.value;
await new Promise((r) => setTimeout(r, 50));
console.log('procurando por', termo);
}
return <input placeholder="Buscar no acervo" onChange={buscar} />;
}Digitando Ra no mesmo campo — duas teclas, dois disparos do onChange:
Uma linha de diferença. Guarde o valor numa constante na primeira linha do
handler e o problema deixa de existir — vale para value, para dataset e para
qualquer coisa que você fosse buscar no elemento depois de um await.
Até aqui, você já sabe: copie do evento os dados necessários antes de
await. A tela pode continuar funcionando mesmo com uma Promise rejeitada, por
isso o console também faz parte da investigação.
Começamos com um botão mínimo e chegamos a reserva por código, formulário, busca por teclado e integração com listeners nativos. O conceito permaneceu o mesmo: entregar uma função agora para responder a uma ação depois.
O que estudar depois de eventos no React?
A lição seguinte, ordem 8, é useState. Eventos informam que algo aconteceu; estado guarda o resultado e faz a interface refletir a mudança.
Com ele, o clique poderá reservar o livro, aumentar o contador e redesenhar a lista em vez de apenas imprimir no console.
Se quiser ver como as peças se encaixam antes de seguir, a trilha de React mostra a ordem completa e o guia de React explica onde cada assunto entra no caminho do zero ao primeiro projeto.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Por que os eventos do React são camelCase e não minúsculos?
Preciso remover o handler quando o componente sai da tela?
Criar a arrow function direto no JSX prejudica a performance?
Dá para usar addEventListener em vez das props do React?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em React 19.2.8 no Chromium 151.0.7922.34, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — Responding to Events — react.dev
- React — Common components: React event object — react.dev
- MDN — Event bubbling and capture — developer.mozilla.org



