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

addEventListener: eventos de clique em JavaScript

Como escutar clique com addEventListener, empilhar mais de um listener, usar once e passive e remover o listener certo — com a saída real de cada clique.

Rodolfo Mori6 min de leitura

addEventListener é o método que registra uma função para responder a um evento da página. Você informa qual elemento será observado, qual evento deve ser ouvido e qual função responderá; o navegador guarda esse registro e chama a função quando o evento acontece.

Pense numa campainha. O botão é o alvo observado, o toque é o evento click e a pessoa que abre a porta é a função. Instalar outra campainha não apaga a primeira: as duas podem tocar. No navegador, cada chamada de addEventListener também acrescenta um registro, e removeEventListener só remove exatamente a função que foi registrada. A comparação termina aí; tecnicamente, tudo acontece por meio da lista de listeners mantida pelo EventTarget.

js
elemento.addEventListener('click', funcao);

Três detalhes decidem se ele vai funcionar ou falhar em silêncio: o nome do evento vai sem o prefixo on, cada chamada soma um listener em vez de substituir o anterior, e removeEventListener só apaga um listener se receber a mesma referência de função que foi registrada. O resto desta lição é cada um desses pontos, rodando.

Os exemplos são de uma loja: botão de adicionar ao carrinho, botão de finalizar pedido e campo de cupom. Assim, cada clique produz um resultado que você pode ver no console.

A assinatura: alvo, evento, função

js
const botao = document.querySelector('#adicionar');

botao.addEventListener('click', function () {
  console.log('Produto adicionado ao carrinho');
});

Clicando duas vezes no botão:

Produto adicionado ao carrinho Produto adicionado ao carrinho

A função que você passa no segundo argumento é o listener — ou handler, ou callback, dependendo de quem está falando. Ela não é chamada por você: fica guardada e o navegador chama quando o clique acontece. É o mesmo mecanismo de funções passadas como argumento que você já viu, só que quem invoca é o navegador.

Repare que passamos function () {...}, sem parênteses depois. Escrever addEventListener('click', funcao()) chama a função na hora e registra o retorno dela como listener — quase sempre undefined.

Um botão, vários listeners

Esta é a razão de addEventListener existir. Cada chamada empilha:

js
const finalizar = document.querySelector('#finalizar');

finalizar.addEventListener('click', () => console.log('1. valida o carrinho'));
finalizar.addEventListener('click', () => console.log('2. envia o evento de analytics'));
finalizar.addEventListener('click', () => console.log('3. abre o checkout'));
1. valida o carrinho 2. envia o evento de analytics 3. abre o checkout

Os três rodaram, na ordem em que foram registrados. Agora o mesmo código com a propriedade onclick:

js
const finalizar = document.querySelector('#finalizar');

finalizar.onclick = () => console.log('1. valida o carrinho');
finalizar.onclick = () => console.log('2. envia o evento de analytics');
finalizar.onclick = () => console.log('3. abre o checkout');
3. abre o checkout

onclick é uma propriedade comum: a segunda atribuição joga a primeira fora, a terceira joga a segunda. Sobrou uma. Num projeto pequeno isso passa despercebido; num projeto com script de analytics, script de chat e seu código, o último a carregar apaga o trabalho dos outros dois — e ninguém recebe erro nenhum.

O nome do evento vai sem on

Quem vem de onclick erra isso na primeira semana:

js
const botao = document.querySelector('#adicionar');

botao.addEventListener('onclick', () => console.log('nunca roda'));
botao.addEventListener('click', () => console.log('esse roda'));
esse roda

O primeiro listener foi registrado com sucesso — para um evento chamado "onclick", que nenhum navegador dispara. Não há erro, não há aviso: o console fica limpo e o botão parece morto. A propriedade é onclick; o nome do evento é click. O mesmo vale para submit, input, change, keydown, mouseover.

Remover exige a mesma referência

removeEventListener compara o elemento, o nome do evento e a função — por identidade, não por conteúdo. Com uma função nomeada, funciona:

js
const cupom = document.querySelector('#cupom');

function aplicarCupom() {
  console.log('cupom FRETEGRATIS aplicado');
  cupom.removeEventListener('click', aplicarCupom);
}

cupom.addEventListener('click', aplicarCupom);

Clicando três vezes:

cupom FRETEGRATIS aplicado

Agora com arrow inline, escrita duas vezes:

js
const pedido = document.querySelector('#pedido');

pedido.addEventListener('click', () => console.log('pedido enviado'));
pedido.removeEventListener('click', () => console.log('pedido enviado'));

Clicando duas vezes:

pedido enviado pedido enviado

O texto das duas funções é idêntico, mas são dois objetos diferentes na memória. O removeEventListener procurou por uma função que nunca foi registrada, não achou, e não reclamou — a especificação manda ignorar em silêncio. Regra prática: se você vai remover, guarde a função numa variável ou dê um nome a ela.

once, capture e passive

O quarto argumento é um objeto de opções. As três que aparecem no dia a dia:

opção o que faz quando usar
once: true o listener roda uma vez e se remove sozinho botão de pagar, aceite de termos
capture: true roda na descida do evento, antes do alvo interceptar antes de outro listener
passive: true promete que você não vai chamar preventDefault scroll, touchstart, wheel

once resolve o clique duplo no botão de pagar sem nenhuma variável de controle:

js
const pagar = document.querySelector('#pagar');

pagar.addEventListener('click', () => console.log('pedido #4412 enviado'), { once: true });

Clicando três vezes:

pedido #4412 enviado

passive: true é uma promessa que o navegador cobra. Ele para de esperar para saber se você vai cancelar a rolagem e já rola a tela — o que faz diferença visível no celular. Se você quebrar a promessa, o preventDefault é ignorado:

js
const catalogo = document.querySelector('#catalogo');

catalogo.addEventListener('touchstart', (evento) => {
  evento.preventDefault();
  console.log('preventDefault teve efeito?', evento.defaultPrevented);
}, { passive: true });
preventDefault teve efeito? false

capture muda a ordem em que os listeners rodam quando há elementos aninhados — o assunto da próxima lição, sobre o objeto event e delegação.

Erros comuns

O elemento não existe: Cannot read properties of null

O único que grita. querySelector devolve null quando não acha nada, e null não tem métodos:

js
const botao = document.querySelector('#finalizar-compra');
botao.addEventListener('click', () => console.log('pedido enviado'));
file:///private/tmp/loja/checkout.mjs:5 botao.addEventListener('click', () => console.log('pedido enviado')); ^

TypeError: Cannot read properties of null (reading ‘addEventListener’) at file:///private/tmp/loja/checkout.mjs:5:7 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

São só duas causas: o seletor está errado (#finalizar-compra quando o HTML tem id="finalizar-pedido") ou o script rodou antes do elemento existir — <script> no <head> sem defer. O diagnóstico completo está em TypeError: Cannot read properties of undefined, que trata da mesma família de erro.

Os parênteses a mais

js
function enviarPedido() {
  console.log('pedido enviado');
}

const pagar = document.querySelector('#pagar');
pagar.addEventListener('click', enviarPedido());
console.log('--- página carregada, ninguém clicou ainda ---');
pedido enviado --- página carregada, ninguém clicou ainda ---

O pedido foi “enviado” no carregamento da página e nunca mais. enviarPedido() executa e devolve undefined; foi undefined que virou listener. Nenhum erro, e o comportamento é o pior possível: acontece na hora errada.

O listener do filho some junto com o innerHTML

Dois listeners, um no <ul> e outro no <button> de dentro dele. No meio do caminho, o HTML da lista é reescrito:

js
const lista = document.querySelector('#carrinho');
const remover = lista.querySelector('.remover');

lista.addEventListener('click', () => console.log('listener do UL respondeu'));
remover.addEventListener('click', () => console.log('listener do BUTTON respondeu'));

lista.innerHTML = '<li data-id="7712">Teclado mecânico <button class="remover">Remover</button></li>';

Clicando no botão “Remover” depois da troca:

listener do UL respondeu

Antes da troca, os dois respondiam. Trocar o innerHTML destrói os filhos e constrói outros a partir do texto: o <button> na tela é novo e nunca recebeu listener nenhum. O do #carrinho sobreviveu porque o <ul> em si não foi recriado. Essa assimetria confunde — o listener no pai resiste, o do filho não — e é mais um motivo para preferir delegação.

Como registrar listener sem se arrepender

  • Uma função nomeada por comportamento. atualizarTotal, não uma arrow anônima de doze linhas dentro do addEventListener. Fica testável e removível.
  • Um listener no container, não um por item. Se a lista tem 40 produtos, 40 listeners fazem o mesmo trabalho que 1 no <ul>.
  • Nada de lógica pesada dentro do listener de scroll, resize ou mousemove. Esses disparam dezenas de vezes por segundo.
  • defer no <script>, ou script no fim do <body>. Metade dos “não funciona” é o script correndo antes do HTML existir.

Para conferir se o modelo ficou claro, crie um botão e registre duas funções nomeadas nele: uma deve mostrar pedido salvo e a outra estoque atualizado. Clique uma vez e confirme as duas linhas no console. Depois remova apenas a primeira função, clique de novo e verifique que só estoque atualizado aparece. Se as duas continuarem rodando, compare a referência passada ao removeEventListener com a registrada no começo.

A próxima lição da trilha de JavaScript abre o objeto event que o navegador entrega para cada listener — de onde saem target, preventDefault e a delegação de eventos. O guia completo de JavaScript mostra onde eventos entram na ordem de estudo.

Prefere aprender em vídeo?

Tem aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • addeventlistener
  • eventos
  • dom
  • clique
  • listener

Perguntas frequentes

Posso continuar usando onclick no HTML?
Funciona, mas custa caro. O atributo onclick no HTML só enxerga funções globais, o que quebra assim que o script vira módulo, e mistura marcação com comportamento. Em código novo, addEventListener no arquivo JS.
Preciso remover os listeners quando a página sai do ar?
Em página comum, não: o navegador descarta tudo junto com o documento. Em SPA, sim — componente que monta e desmonta várias vezes e nunca remove o listener acumula um handler por montagem e vaza memória.
Qual a diferença entre listener no elemento e listener no document?
O listener no document pega qualquer clique da página e você decide dentro dele o que fazer. É a base da delegação de eventos, e a única forma de atender elementos que ainda nem existem quando o script roda.
addEventListener funciona com evento que eu mesmo criei?
Funciona. Qualquer string serve como nome de evento, e você dispara com dispatchEvent passando um CustomEvent. É como componentes de um mesmo projeto conversam sem precisar se conhecer.

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 Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — EventTarget: método addEventListener() — developer.mozilla.org
  2. MDN — Introdução a eventos — developer.mozilla.org
  3. DOM Standard — Interface EventTarget — dom.spec.whatwg.org

Continue por aqui