Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori14 min de leitura

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.

jsx
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:

--- Estante renderizou --- reserva registrada --- clique no primeiro botão --- reserva registrada --- clique no segundo botão ---

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.

jsx
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”:

reservando VS-07 a partir de <BUTTON> reservando DC-14 a partir de <BUTTON>

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:

jsx
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”:

constructor.name : SyntheticBaseEvent type : click target : SPAN "Dom Casmurro" currentTarget : BUTTON bubbles : true nativeEvent : PointerEvent é o mesmo type? : true isTrusted : true

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.

jsx
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:

handler do React rodou [navegou para] /?leitor=Ana+Prado

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é:

jsx
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>
  );
}
handler do React rodou leitor: Ana Prado defaultPrevented: true

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:

jsx
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>
  );
}
abrindo no leitor interno: /acervo/dom-casmurro URL depois do clique: /

Tirando a linha do preventDefault, o mesmo clique produz isto — o navegador foi embora para uma rota que não existe:

abrindo no leitor interno: /acervo/dom-casmurro [navegou para] /acervo/dom-casmurro Failed to load resource: the server responded with a status of 404 (Not Found) URL depois do clique: /acervo/dom-casmurro

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:

jsx
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:

1. captura — section.estante 2. captura — article.ficha 3. captura — button 4. bolha — button 5. bolha — article.ficha 6. bolha — section.estante

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.

jsx
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>
  );
}
# botão sem stopPropagation cancelou a reserva de Dom Casmurro abriu a ficha de Dom Casmurro # botão com stopPropagation cancelou a reserva de Vidas Secas

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:

jsx
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:

[onChange React] g [onChange React] gr [onChange React] gra [onChange React] grac [onChange React] graci # agora o campo perde o foco [change nativo] graci

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:

jsx
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:

foco: entrou no campo keydown: key=R ctrl=false shift=false keydown: key=a ctrl=false shift=false keydown: key=Enter ctrl=false shift=false buscando por "Ra" keydown: key=Escape ctrl=false shift=false campo limpo keydown: key=Tab ctrl=false shift=false blur: saiu com ""

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:

<button> o clique nasce aqui div#root o único listener real React chama os seus handlers sobe reconstrói

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:

js
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:

Chromium 151.0.7922.34 getEventListeners($('#com')) -> 1 listener(s): ["click"] getEventListeners($('#sem')) -> 0 listener(s): [] $('#com').onclick.toString() -> function noop$1() { } getEventListeners($('#root')) -> 139 listeners, em 85 tipos de evento 53 tipos nas duas fases, 32 tipos só na captura click: click/bolha, click/captura, click/bolha change: change/bolha, change/captura keydown:keydown/bolha, keydown/captura scroll: scroll/captura

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:

jsx
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'));
}, []);
document — captura React — onClickCapture React — onClick 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:

jsx
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:

Uncaught (in promise) TypeError: Cannot read properties of null (reading 'value') at buscar (BuscaAcervo.jsx:6:56)

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:

jsx
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);
}
antes do await : BUTTON BUTTON depois do await: undefined BUTTON

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:

jsx
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:

procurando por R procurando por Ra

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.

Ver todos os vídeos do canal
  • react
  • eventos
  • onclick
  • onchange
  • dom

Perguntas frequentes

Por que os eventos do React são camelCase e não minúsculos?
Porque no JSX você não está escrevendo atributo HTML, e sim propriedade de um objeto JavaScript. O React usa a convenção da linguagem, camelCase, do mesmo jeito que usa className no lugar de class.
Preciso remover o handler quando o componente sai da tela?
Não, se você usou a prop onClick. O React desfaz o vínculo junto com o elemento. Só precisa limpar o que você registrou na mão com addEventListener dentro de um efeito.
Criar a arrow function direto no JSX prejudica a performance?
Na prática, não em tela de aplicação normal. Uma função nova por render é barata. Isso só vira assunto quando o filho é memorizado e passa a re-renderizar à toa por causa da prop nova.
Dá para usar addEventListener em vez das props do React?
Dá, com useRef e useEffect, e às vezes é obrigatório — listener passivo, evento em window ou em document. Para o que acontece dentro do seu JSX, a prop é mais simples e o React cuida da remoção.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. React — Responding to Events — react.dev
  2. React — Common components: React event object — react.dev
  3. MDN — Event bubbling and capture — developer.mozilla.org

Continue por aqui