Back

Guía para Desarrolladores sobre Eventos Personalizados en JavaScript

Guía para Desarrolladores sobre Eventos Personalizados en JavaScript

Has construido una arquitectura de componentes limpia, pero ahora las piezas necesitan comunicarse entre sí sin crear un acoplamiento estrecho. Los eventos nativos del DOM manejan bien las interacciones del usuario, pero ¿qué pasa con las señales específicas de tu aplicación? Los Eventos Personalizados de JavaScript resuelven este problema de manera elegante.

Esta guía cubre la creación y el envío de eventos personalizados, el paso de datos estructurados a través de la carga útil detail de CustomEvent, y el uso de EventTarget como un bus de eventos ligero. También aprenderás cómo funcionan los eventos personalizados en Web Components y cómo se propagan los eventos personalizados del Shadow DOM a través de los límites.

Puntos Clave

  • Usa el constructor CustomEvent con la propiedad detail para pasar datos estructurados a través de eventos
  • La interfaz EventTarget funciona de forma independiente, permitiendo patrones pub/sub ligeros sin elementos DOM
  • Establece bubbles: true para eventos que deban propagarse hacia arriba en el árbol DOM
  • Usa composed: true para permitir que los eventos crucen los límites del Shadow DOM en Web Components

Creación y Envío de Eventos Personalizados

El constructor CustomEvent es la forma moderna de crear eventos personalizados. Olvídate de initCustomEvent—esa es una API heredada que no necesitarás en navegadores modernos.

const event = new CustomEvent('user-login', {
  detail: { userId: 123, timestamp: Date.now() },
  bubbles: true,
  cancelable: true
})

document.querySelector('#app').dispatchEvent(event)

Tres opciones importan aquí:

  • detail: Tu carga útil estructurada (cualquier dato serializable)
  • bubbles: Si el evento se propaga hacia arriba en el árbol DOM
  • cancelable: Si los listeners pueden llamar a preventDefault()

Escuchar funciona exactamente como con los eventos nativos:

document.querySelector('#app').addEventListener('user-login', (e) => {
  console.log(e.detail.userId) // 123
})

La Carga Útil Detail de CustomEvent

La propiedad detail es donde los eventos personalizados brillan sobre el constructor básico Event. Aunque técnicamente podrías asignar propiedades arbitrarias a un objeto Event después de su creación, detail proporciona un espacio de nombres dedicado y libre de conflictos para tus datos.

const cartEvent = new CustomEvent('cart-updated', {
  detail: {
    items: [{ id: 1, qty: 2 }, { id: 3, qty: 1 }],
    total: 59.99,
    currency: 'USD'
  }
})

Los manejadores acceden a esto a través de event.detail—limpio y predecible.

EventTarget del DOM como Bus de Eventos

No necesitas un elemento DOM para usar eventos. La interfaz EventTarget funciona de forma independiente, haciéndola perfecta para un mecanismo pub/sub ligero:

class AppEventBus extends EventTarget {
  emit(eventName, data) {
    this.dispatchEvent(new CustomEvent(eventName, { detail: data }))
  }
  
  on(eventName, handler) {
    this.addEventListener(eventName, handler)
  }
  
  off(eventName, handler) {
    this.removeEventListener(eventName, handler)
  }
}

const bus = new AppEventBus()
bus.on('notification', (e) => console.log(e.detail.message))
bus.emit('notification', { message: 'Hello!' })

Este patrón de bus de eventos basado en EventTarget mantiene los componentes desacoplados sin dependencias externas. Para usuarios de TypeScript, puedes tipar la carga útil detail usando genéricos: CustomEvent<{ message: string }>.

Nota: En Node.js, EventTarget existe pero CustomEvent solo está disponible como global en Node 19+. El EventEmitter de Node sigue siendo el patrón más común allí.

Eventos Personalizados en Web Components

Los Web Components dependen en gran medida de eventos personalizados para la comunicación hacia afuera. Un componente envía eventos mientras el código padre escucha:

class UserCard extends HTMLElement {
  connectedCallback() {
    this.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('user-selected', {
        detail: { id: this.dataset.userId },
        bubbles: true,
        composed: true
      }))
    })
  }
}
customElements.define('user-card', UserCard)

El padre escucha sin conocer los detalles internos del componente:

document.querySelector('user-card').addEventListener('user-selected', (e) => {
  loadUserProfile(e.detail.id)
})

Eventos Personalizados del Shadow DOM y la Opción Composed

Los eventos personalizados del Shadow DOM se comportan de manera diferente según la opción composed:

  • composed: false (predeterminado): El evento se detiene en el límite de la raíz shadow. La implementación interna permanece oculta.
  • composed: true: El evento cruza los límites shadow y burbujea a través del DOM ligero.
// Dentro del shadow DOM
this.shadowRoot.querySelector('button').dispatchEvent(
  new CustomEvent('internal-action', {
    bubbles: true,
    composed: true // Escapa del límite shadow
  })
)

Cuando un evento cruza el límite shadow, event.target se redirige al elemento host—los listeners externos ven el componente, no su estructura interna.

Usa composed: true para eventos que el código externo deba manejar. Mantén composed: false para comunicación interna del componente.

Una nota rápida sobre event.isTrusted: esta propiedad indica si el navegador (acción del usuario) o un script generó el evento. Es informativa, no un mecanismo de seguridad—no confíes en ella para control de acceso.

Conclusión

Los Eventos Personalizados de JavaScript proporcionan una forma agnóstica al framework de construir arquitecturas orientadas a eventos y débilmente acopladas. Usa CustomEvent con una carga útil detail estructurada para datos, aprovecha EventTarget como un bus de eventos independiente, y comprende cómo bubbles y composed controlan la propagación—especialmente a través de los límites del Shadow DOM. Estos patrones escalan desde la comunicación simple de componentes hasta arquitecturas complejas de micro-frontends.

Preguntas Frecuentes

La principal diferencia es la propiedad detail. CustomEvent incluye una propiedad detail dedicada para pasar datos estructurados, mientras que el constructor básico Event no la tiene. Aunque puedes agregar propiedades a un objeto Event después de su creación, usar CustomEvent con detail es más limpio y evita posibles conflictos de nombres con propiedades de eventos existentes.

Sí, pero con advertencias. React no soporta nativamente listeners de eventos personalizados en JSX, por lo que necesitarás usar refs y adjuntar listeners manualmente con addEventListener. Vue maneja mejor los eventos personalizados a través de su sistema de eventos. Los eventos personalizados funcionan mejor cuando se comunican entre componentes de framework y Web Components vanilla o código agnóstico al framework.

No. Establece bubbles en true solo cuando los elementos padre necesiten capturar el evento. Para comunicación directa donde envías y escuchas en el mismo elemento, el burbujeo es innecesario. El burbujeo excesivo puede llevar a que listeners no deseados capturen eventos, así que sé intencional sobre la propagación.

Usa removeEventListener con exactamente la misma referencia de función que pasaste a addEventListener. Las funciones anónimas no pueden eliminarse porque no hay referencia que pasar. Almacena tu función manejadora en una variable o usa el patrón AbortController con la opción signal para una limpieza más clara.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay