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.
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.
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
const botao = document.querySelector('#adicionar');
botao.addEventListener('click', function () {
console.log('Produto adicionado ao carrinho');
});Clicando duas vezes no botão:
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:
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'));Os três rodaram, na ordem em que foram registrados. Agora o mesmo código com a
propriedade onclick:
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');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:
const botao = document.querySelector('#adicionar');
botao.addEventListener('onclick', () => console.log('nunca roda'));
botao.addEventListener('click', () => console.log('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:
const cupom = document.querySelector('#cupom');
function aplicarCupom() {
console.log('cupom FRETEGRATIS aplicado');
cupom.removeEventListener('click', aplicarCupom);
}
cupom.addEventListener('click', aplicarCupom);Clicando três vezes:
Agora com arrow inline, escrita duas vezes:
const pedido = document.querySelector('#pedido');
pedido.addEventListener('click', () => console.log('pedido enviado'));
pedido.removeEventListener('click', () => console.log('pedido enviado'));Clicando duas vezes:
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:
const pagar = document.querySelector('#pagar');
pagar.addEventListener('click', () => console.log('pedido #4412 enviado'), { once: true });Clicando três vezes:
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:
const catalogo = document.querySelector('#catalogo');
catalogo.addEventListener('touchstart', (evento) => {
evento.preventDefault();
console.log('preventDefault teve efeito?', evento.defaultPrevented);
}, { passive: true });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:
const botao = document.querySelector('#finalizar-compra');
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
function enviarPedido() {
console.log('pedido enviado');
}
const pagar = document.querySelector('#pagar');
pagar.addEventListener('click', enviarPedido());
console.log('--- 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:
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:
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 doaddEventListener. 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,resizeoumousemove. Esses disparam dezenas de vezes por segundo. deferno<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.
DevClub no YouTubeJavaScript WEB e a DOM na Prática | Curso de JavaScript Completo GratuitoAssistir a aula
DevClub no YouTubeTrocar Cor De Fundo Ao Clicar | Projeto CSS, HTML e JavascriptAssistir a aula
Perguntas frequentes
Posso continuar usando onclick no HTML?
Preciso remover os listeners quando a página sai do ar?
Qual a diferença entre listener no elemento e listener no document?
addEventListener funciona com evento que eu mesmo criei?
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 Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — EventTarget: método addEventListener() — developer.mozilla.org
- MDN — Introdução a eventos — developer.mozilla.org
- DOM Standard — Interface EventTarget — dom.spec.whatwg.org


