Em aplicações JavaScript modernas, especialmente aquelas construídas com componentes, a comunicação entre diferentes partes do sistema é essencial. Embora possamos usar callbacks, promessas ou gerenciamento de estado, os eventos customizados oferecem uma abordagem desacoplada e flexível. Eles permitem que um componente emita um evento que outros componentes podem ouvir, sem que haja uma referência direta entre eles. Nesta aula, vamos explorar a API de eventos customizados do JavaScript: como criar eventos personalizados com CustomEvent, como dispará-los com dispatchEvent e como usá-los para comunicação entre componentes.

O modelo de eventos do JavaScript já é amplamente usado para eventos nativos como click, keydown e load. Eventos customizados estendem esse modelo, permitindo que você defina seus próprios tipos de evento com dados associados. Isso é particularmente útil em arquiteturas baseadas em componentes, onde a comunicação direta entre componentes pode criar acoplamento desnecessário.

CustomEvent

A interface CustomEvent é uma extensão da interface Event que permite criar eventos com dados personalizados. Ela é usada para criar um objeto de evento que pode ser disparado em qualquer elemento DOM. O construtor CustomEvent aceita dois argumentos: o tipo do evento (uma string) e um objeto de opções opcional.

O objeto de opções pode conter as propriedades padrão de Event (como bubbles, cancelable, composed) e também uma propriedade detail que carrega os dados personalizados que você deseja transmitir. Esses dados podem ser qualquer valor JavaScript: um objeto, uma string, um número, etc.

Vejamos um exemplo simples de criação de um evento customizado:

// Cria um evento customizado chamado 'userLogin' com dados de usuário
const loginEvent = new CustomEvent('userLogin', {
  detail: {
    username: 'joaosilva',
    timestamp: Date.now()
  },
  bubbles: true,
  cancelable: true
});

No exemplo acima, criamos um evento do tipo userLogin com um objeto detail contendo informações do usuário. A opção bubbles define se o evento irá propagar pela árvore DOM, e cancelable indica se o evento pode ser cancelado (útil se você quiser permitir que listeners cancelem a ação padrão).

Uma característica importante é que o evento customizado pode ser disparado em qualquer elemento DOM, e ele irá percorrer o DOM de acordo com as opções de propagação. Isso significa que você pode ter listeners em elementos ancestrais que capturam o evento.

dispatchEvent

O método dispatchEvent é usado para disparar um evento em um elemento DOM. Ele está disponível em todos os elementos DOM (via Element.prototype) e também no window, document e outros objetos que implementam a interface EventTarget. Quando você chama dispatchEvent, o evento é entregue ao alvo e, se bubbles for verdadeiro, ele sobe pela árvore DOM até o topo.

Para disparar o evento criado no exemplo anterior, fazemos:

// Dispara o evento no elemento 'loginButton'
const loginButton = document.getElementById('loginButton');
loginButton.dispatchEvent(loginEvent);

Normalmente, você dispara um evento em resposta a uma ação do usuário ou a uma mudança de estado. Por exemplo, quando um usuário faz login, você pode disparar um evento userLogin no elemento raiz da aplicação ou em um elemento específico para notificar outros componentes.

É importante notar que dispatchEvent é síncrono. Ou seja, os listeners são chamados imediatamente durante a chamada de dispatchEvent. Isso pode ser vantajoso para garantir que a reação ao evento ocorra antes de continuar a execução.

Outra utilidade é que você pode disparar eventos em qualquer elemento, não necessariamente no elemento que originou a ação. Por exemplo, você pode disparar um evento no document para que todos os componentes possam ouvir.

// Disparando um evento global no document
document.dispatchEvent(new CustomEvent('app:init'));

Isso é comum em aplicações onde você quer que qualquer parte do sistema reaja a uma inicialização global.

Comunicação entre componentes

Eventos customizados são uma excelente ferramenta para comunicação entre componentes, especialmente em arquiteturas como Web Components ou em frameworks que não impõem um padrão de comunicação (como React, mas com eventos). O princípio é que um componente emite um evento e outros componentes podem escutá-lo, sem precisar conhecer a origem diretamente.

Vamos imaginar um cenário em que temos um componente de cabeçalho com um botão de busca e um componente de lista de resultados. Quando o usuário digita uma busca, o cabeçalho deve notificar a lista para atualizar os resultados. Em vez de passar referências ou usar um estado global, podemos usar um evento customizado.

Primeiro, definimos o evento no cabeçalho:

// Componente de cabeçalho
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');

searchButton.addEventListener('click', () => {
  const query = searchInput.value;
  // Cria e dispara um evento de busca
  const searchEvent = new CustomEvent('search:query', {
    detail: { query },
    bubbles: true
  });
  searchButton.dispatchEvent(searchEvent);
});

No componente de lista, escutamos o evento:

// Componente de lista de resultados
document.addEventListener('search:query', (event) => {
  const { query } = event.detail;
  // Atualiza a lista com os resultados da busca
  fetchResults(query).then(renderResults);
});

Perceba que o componente de lista não precisa saber de onde vem o evento; ele apenas escuta no document. Isso é uma forma de comunicação desacoplada.

Outra abordagem é usar um elemento central como um barramento de eventos (event bus). Você pode criar um objeto que serve como um hub para eventos:

// Event bus simples
const EventBus = {
  emit(eventName, detail) {
    const event = new CustomEvent(eventName, { detail });
    document.dispatchEvent(event);
  },
  on(eventName, callback) {
    document.addEventListener(eventName, callback);
  }
};

// Uso
EventBus.emit('user:login', { user: 'joao' });
EventBus.on('user:login', (e) => {
  console.log('Usuário logado:', e.detail.user);
});

Essa abordagem permite que qualquer componente envie e receba eventos sem criar dependências diretas.

É importante lembrar que eventos customizados são síncronos e podem ser cancelados. Se você precisar de comunicação assíncrona ou de um padrão mais robusto, considere usar outras técnicas, como Promises ou gerenciamento de estado.

Boas práticas

Ao usar eventos customizados, siga algumas boas práticas para manter o código limpo e previsível:

  • Use nomes de eventos descritivos e consistentes, como user:login ou cart:add. Evite nomes genéricos como event1.
  • Documente os eventos que seus componentes emitem, incluindo a estrutura de detail, para que outros desenvolvedores saibam como usá-los.
  • Evite abusar de eventos customizados para comunicação excessiva. Se dois componentes estão sempre se comunicando, talvez seja melhor ter uma relação mais direta.
  • Considere usar bubbles: true para que eventos possam ser capturados em níveis mais altos, mas lembre-se de que isso pode causar escuta acidental em outros componentes.
  • Para eventos globais, prefira dispará-los no document ou em um elemento raiz, e use nomes com prefixo (como app:) para evitar colisões.

Referências

Exercícios

  1. Crie um evento customizado chamado cart:add que carrega em detail um objeto com productId e quantity. Dispare esse evento em um botão de adicionar ao carrinho.

    ✓ Resposta:
    // Supondo que exista um botão com id 'add-to-cart'
    const addButton = document.getElementById('add-to-cart');
    addButton.addEventListener('click', () => {
      const event = new CustomEvent('cart:add', {
        detail: {
          productId: 123,
          quantity: 1
        },
        bubbles: true
      });
      addButton.dispatchEvent(event);
    });
  2. Escreva um listener para o evento do exercício anterior que exiba no console a mensagem "Produto 123 adicionado ao carrinho (quantidade: 1)". Use document.addEventListener.

    ✓ Resposta:
    document.addEventListener('cart:add', (event) => {
      const { productId, quantity } = event.detail;
      console.log(`Produto ${productId} adicionado ao carrinho (quantidade: ${quantity})`);
    });
  3. Implemente um pequeno sistema de eventos onde um componente 'header' dispara um evento 'search:submit' com o termo de busca, e outro componente 'results' escuta esse evento e atualiza um elemento <div> com o termo recebido. Use um event bus simples.

    ✓ Resposta:
    // Event bus
    const EventBus = {
      emit(eventName, detail) {
        document.dispatchEvent(new CustomEvent(eventName, { detail }));
      },
      on(eventName, callback) {
        document.addEventListener(eventName, callback);
      }
    };
    
    // Header component
    const searchInput = document.getElementById('search-input');
    const searchButton = document.getElementById('search-button');
    searchButton.addEventListener('click', () => {
      EventBus.emit('search:submit', { term: searchInput.value });
    });
    
    // Results component
    const resultsDiv = document.getElementById('results');
    EventBus.on('search:submit', (event) => {
      resultsDiv.textContent = `Resultados para: ${event.detail.term}`;
    });
  4. Explique a diferença entre bubbles: true e bubbles: false em um evento customizado. Dê um exemplo de quando você usaria cada um.

    ✓ Resposta:

    Quando bubbles é true, o evento propaga do elemento alvo para cima na árvore DOM, permitindo que listeners em elementos ancestrais (como document) o capturem. Isso é útil para comunicação global ou para capturar eventos em um nível mais alto. Quando bubbles é false, o evento só é entregue ao elemento alvo; listeners em ancestrais não serão acionados. Isso é útil para eventos que devem ser tratados apenas pelo próprio elemento, evitando efeitos colaterais em outros componentes.

    Exemplo: um evento input:change disparado em um campo de formulário pode ter bubbles: false se apenas o componente do formulário precisa reagir. Já um evento user:logout pode ter bubbles: true para que toda a aplicação possa reagir.

  5. Crie um evento customizado que seja cancelável e escreva um listener que chame event.preventDefault() para cancelar a ação padrão. Demonstre como verificar se o evento foi cancelado após o dispatch.

    ✓ Resposta:
    // Cria um evento cancelável
    const cancelableEvent = new CustomEvent('cancelable-action', {
      cancelable: true
    });
    
    // Listener que cancela o evento
    document.addEventListener('cancelable-action', (event) => {
      event.preventDefault();
      console.log('Evento cancelado!');
    });
    
    // Dispara o evento e verifica se foi cancelado
    const wasCancelled = !document.dispatchEvent(cancelableEvent);
    console.log('Foi cancelado?', wasCancelled); // true

    O método dispatchEvent retorna false se o evento foi cancelado por algum listener (via preventDefault()), e true caso contrário.

Observações finais

Eventos customizados são uma ferramenta poderosa para criar aplicações modulares e desacopladas. Eles permitem que diferentes partes do sistema se comuniquem de forma limpa, sem criar dependências diretas. Aprender a usar CustomEvent e dispatchEvent é essencial para desenvolvedores que trabalham com componentes, seja em Web Components, frameworks ou JavaScript puro.

Lembre-se de sempre documentar os eventos e seus dados, e de usar nomes significativos. Com a prática, você verá como essa técnica pode simplificar a arquitetura do seu código.