JS
() =>
JavaScript · Custom Events · DOM · Komponentenarchitektur
JavaScript Custom Events
Komponentenkommunikation ohne Framework

Props-Drilling und globale Variablen sind Zeichen enger Kopplung. Custom Events bieten eine native DOM-Lösung: Komponenten senden Events, andere hören zu – ohne direkte Referenz aufeinander, ohne Framework, mit vollem TypeScript-Support.

12 Min. Lesezeit CustomEvent · dispatchEvent · bubbles · detail · Event Bus Vanilla JS · TypeScript · Alpine.js · Web Components

1. Das Problem der Komponentenkommunikation

In wachsenden Web-Anwendungen entsteht früher oder später das Problem der Komponentenkommunikation: Ein Warenkorb-Icon in der Navigation muss wissen, wenn ein Produkt hinzugefügt wurde. Ein Toast-System muss reagieren, wenn irgendwo auf der Seite ein Fehler auftritt. Ein Filter-Panel muss die Produktliste aktualisieren. Die naheliegende Lösung – direkte Referenzen zwischen Komponenten – schafft enge Kopplung: Komponente A muss Komponente B kennen, Änderungen an B erfordern Änderungen in A. Skaliert schlecht, ist schwer zu testen.

Custom Events lösen dieses Problem mit einem DOM-nativen Publish-Subscribe-Mechanismus. Eine Komponente dispatcht ein Event, ohne zu wissen, wer zuhört. Andere Komponenten hören auf dieses Event, ohne zu wissen, wer es ausgelöst hat. Das Ergebnis ist lose Kopplung: Komponenten können unabhängig entwickelt, getestet und ausgetauscht werden. Custom Events sind kein Framework-Feature, sondern ein Teil der DOM-Spezifikation – verfügbar in allen modernen Browsern, ohne externe Abhängigkeiten, ohne Build-Schritt.

2. CustomEvent: Aufbau und Grundprinzip

Die CustomEvent-API ist eine Erweiterung der nativen Event-Klasse. Sie wird mit einem Ereignisnamen und einem optionalen Konfigurationsobjekt erstellt und mit element.dispatchEvent(event) auf einem DOM-Element ausgelöst. Die minimale Syntax: new CustomEvent('cart:item-added'). Das resultierende Event verhält sich wie jedes native Browser-Event – es kann auf einem Element ausgelöst, mit addEventListener empfangen und mit stopPropagation() gestoppt werden.

Konvention für Custom Events ist ein Namensschema mit Doppelpunkt als Trenner: namespace:action. Das verhindert Konflikte mit nativen Browser-Events (die kein Doppelpunkt verwenden) und macht den Ursprung und die Bedeutung eines Events im Code sofort erkennbar. Beispiele: cart:item-added, modal:opened, form:validated, auth:signed-in. Konsistente Benennung ist besonders wichtig, wenn mehrere Teams an derselben Codebasis arbeiten – ein dokumentierter Event-Katalog verhindert Naming-Konflikte und macht die Architektur navigierbar.


// Dispatching a basic CustomEvent on a DOM element
const productCard = document.querySelector('.product-card');

productCard.dispatchEvent(
  new CustomEvent('cart:item-added', {
    bubbles: true,       // Event bubbles up the DOM tree
    cancelable: false,   // Cannot be prevented with event.preventDefault()
    composed: false,     // Does not cross Shadow DOM boundaries
    detail: {
      productId: 'SKU-9876',
      name: 'Mironsoft T-Shirt',
      price: 29.99,
      quantity: 1,
    },
  })
);

// Listening anywhere up the DOM tree (thanks to bubbling)
document.addEventListener('cart:item-added', (event) => {
  const { productId, name, price, quantity } = event.detail;
  updateCartUI({ productId, name, price, quantity });
  showAddedToCartToast(name);
});

3. Daten transportieren mit detail

Das detail-Property ist der Payload-Mechanismus von Custom Events. Es akzeptiert jeden serialisierbaren JavaScript-Wert – Objekte, Arrays, primitive Werte. Der Listener empfängt die Daten über event.detail. Ein wichtiges Detail: detail ist in bestimmten Browserkontexten (Cross-Origin-iframes, SharedWorker) strukturell geklont – ein Algorithmus, der viele Objekte unterstützt, aber keine Functions, DOM-Nodes oder Promises. Für normale Same-Origin-Kommunikation sind diese Einschränkungen irrelevant.

Eine häufige Frage beim Einsatz von Custom Events: Sollen Daten per detail oder über eine direkte Referenz auf ein Zustandsobjekt übermittelt werden? Die Empfehlung ist, immutable Snapshots im detail zu übergeben – also flache Kopien der relevanten Zustandsdaten zum Zeitpunkt des Events. Das macht Events nachvollziehbar und testbar: Der Listener erhält alle relevanten Informationen aus dem Event selbst, ohne globalen Zustand lesen zu müssen. Das folgt dem Event-Sourcing-Prinzip und macht Debugging mit console.log(event.detail) unmittelbar hilfreich.

4. Bubbling und Capturing verstehen

Ob ein Custom Event bubbling verwendet, entscheidet über die Architektur der Listener-Registrierung. Mit bubbles: true propagiert das Event von seinem Ursprungselement nach oben durch alle Eltern bis zum document. Das ermöglicht es, Listener an zentraler Stelle zu registrieren – zum Beispiel document.addEventListener('cart:item-added', handler) – anstatt auf jedem einzelnen Produkt-Element. Ohne bubbles: true (der Standard) muss der Listener genau auf dem Element registriert sein, das das Event dispatcht.

Ein wichtiges Szenario bei Custom Events und Bubbling: Performance. Wenn hunderte gleichartige Elemente Custom Events dispatchen können, ist ein einzelner delegierter Listener am document effizienter als ein Listener pro Element. Gleichzeitig birgt globales Bubbling das Risiko, dass Events unbeabsichtigt andere Komponenten erreichen. Mit event.stopPropagation() lässt sich die Ausbreitung gezielt stoppen. Das Capturing-Modell – Listener mit dem dritten Parameter true in addEventListener – erlaubt es, Events abzufangen, bevor sie das Zielelement erreichen, was für Authorization-Patterns und globale Guard-Mechanismen nützlich ist.


// Event delegation with bubbling — one listener for all product cards
document.addEventListener('cart:item-added', (event) => {
  // event.target: the element that dispatched the event
  // event.currentTarget: the element with the listener (document here)
  console.log('Dispatched from:', event.target.dataset.productId);
  console.log('Item:', event.detail);

  updateCartCount(event.detail.quantity);
});

// Stop propagation — prevent event from reaching document
const modal = document.querySelector('.modal');
modal.addEventListener('cart:item-added', (event) => {
  handleModalCartAction(event.detail);
  event.stopPropagation(); // Does not bubble further up to document listener
});

// Capturing: intercept before reaching target
document.addEventListener('cart:item-added', (event) => {
  if (!isUserLoggedIn()) {
    event.stopImmediatePropagation(); // Block all other listeners
    showLoginPrompt();
  }
}, true); // true = capturing phase

// composed: true — cross Shadow DOM boundaries (for Web Components)
shadowHost.dispatchEvent(
  new CustomEvent('web-component:ready', { bubbles: true, composed: true })
);

5. Event-Listener richtig verwalten

Ein häufiges Problem mit Custom Events in Single-Page-Anwendungen und dynamischen Seiten ist das Vergessen, Listener zu entfernen. Wenn eine Komponente beim Mounten einen Event-Listener registriert, beim Unmounten aber vergisst, ihn zu entfernen, entstehen Memory-Leaks und doppelte Event-Verarbeitung bei erneutem Mounten. removeEventListener erfordert eine Referenz auf dieselbe Funktion, die an addEventListener übergeben wurde – anonyme Arrow Functions können nicht entfernt werden.

Das sauberste Pattern für die Verwaltung von Custom-Event-Listenern in modernem JavaScript ist AbortController mit AbortSignal. Seit 2021 akzeptiert addEventListener ein signal-Objekt. Wenn das Signal abgebrochen wird (controller.abort()), werden alle damit registrierten Listener automatisch entfernt – ohne jede Funktionsreferenz merken zu müssen. Ein einziger AbortController kann alle Listener einer Komponente verwalten und macht Cleanup zu einer Einzeiler-Operation.


// AbortController-based listener cleanup — modern pattern
class CartWidget {
  #controller = new AbortController();

  mount() {
    const { signal } = this.#controller;

    // All listeners share the same signal
    document.addEventListener('cart:item-added', this.#handleItemAdded, { signal });
    document.addEventListener('cart:item-removed', this.#handleItemRemoved, { signal });
    document.addEventListener('auth:signed-out', this.#handleSignOut, { signal });

    // Signal is also passed to fetch for cancellable requests
    fetch('/api/cart', { signal }).then(/* ... */);
  }

  unmount() {
    // Single call removes ALL listeners registered with this signal
    this.#controller.abort();
  }

  #handleItemAdded = (event) => {
    this.#render(event.detail);
  };

  #handleItemRemoved = (event) => {
    this.#removeItem(event.detail.productId);
  };

  #handleSignOut = () => {
    this.#clearCart();
  };
}

6. Typsicherer Event-Katalog

Ohne Typisierung können Custom Events schnell zur Quelle subtiler Bugs werden: ein Tippfehler im Event-Namen, eine falsche Annahme über die Struktur von event.detail, ein nicht mehr verwendetes Event, das immer noch dispatcht wird. TypeScript löst dieses Problem mit einem zentralen Event-Katalog, der alle Custom Events einer Anwendung mit ihren Detail-Typen deklariert. Durch Interface-Merging mit dem WindowEventMap (oder einem Custom-Element-Event-Map) erhält man vollständige Typprüfung und Autovervollständigung für alle Custom-Event-Zugriffe.

Ein Event-Katalog als TypeScript-Interface dient gleichzeitig als lebende Dokumentation: Alle Custom Events der Anwendung sind an einem Ort aufgelistet, mit ihren Payloads und ihrer Bedeutung. Neu entwickelte Komponenten können den Katalog konsultieren, um zu prüfen, welche Events bereits existieren. Beim Entfernen eines Events zeigt der TypeScript-Compiler alle Stellen, die auf dieses Event hören – das macht Refactoring sicher. Das ist ein wesentlicher Unterschied zu einem String-basierten Event-System ohne Typen.


// TypeScript: type-safe custom event catalog
// Define all custom events and their detail types in one place

interface AppEventMap {
  'cart:item-added': { productId: string; name: string; price: number; quantity: number };
  'cart:item-removed': { productId: string };
  'cart:cleared': Record<string, never>;
  'modal:opened': { modalId: string; trigger: string };
  'modal:closed': { modalId: string; confirmed: boolean };
  'auth:signed-in': { userId: string; email: string };
  'auth:signed-out': Record<string, never>;
  'form:submitted': { formId: string; data: Record<string, unknown> };
}

// Type-safe dispatch helper
function dispatch<K extends keyof AppEventMap>(
  target: EventTarget,
  event: K,
  detail: AppEventMap[K],
  options: { bubbles?: boolean; composed?: boolean } = { bubbles: true }
): void {
  target.dispatchEvent(new CustomEvent(event, { detail, ...options }));
}

// Type-safe listen helper
function listen<K extends keyof AppEventMap>(
  target: EventTarget,
  event: K,
  handler: (detail: AppEventMap[K], event: CustomEvent) => void,
  signal?: AbortSignal
): void {
  target.addEventListener(
    event,
    (e: Event) => handler((e as CustomEvent<AppEventMap[K]>).detail, e as CustomEvent),
    { signal }
  );
}

// Usage — fully typed, no manual casting
dispatch(document, 'cart:item-added', { productId: 'SKU-001', name: 'T-Shirt', price: 29.99, quantity: 1 });
listen(document, 'cart:item-added', ({ productId, price }) => {
  console.log(productId, price); // TypeScript knows the types
});

7. Event-Bus-Pattern für globale Kommunikation

Für Szenarien, in denen Events nicht über den DOM-Baum kommunizieren sollen – etwa zwischen unverbundenen Komponenten auf derselben Seite, zwischen einem Main Thread und einem Service Worker, oder zwischen Web Workers – bietet sich ein Event-Bus-Pattern an. Ein Event-Bus ist ein einzelnes, gemeinsam genutztes Objekt, das als Event-Target fungiert. Alle Komponenten dispatchen auf diesem gemeinsamen Target und hören dort. Das ist konzeptuell einfacher als das globale document-Objekt als Bus zu verwenden, weil der Event-Bus explizit und kontrollierbar ist.

In modernem JavaScript lässt sich ein Event-Bus mit einem einzigen EventTarget-Objekt bauen: const bus = new EventTarget(). Das EventTarget-Interface unterstützt addEventListener, removeEventListener und dispatchEvent – alles was man für Custom Events braucht. Dieser leichtgewichtige Ansatz vermeidet alle Overhead einer dedizierten Event-Bus-Library und nutzt die native DOM-API. Der Event-Bus kann als ES-Modul exportiert und in allen Komponenten importiert werden, die globale Kommunikation benötigen.

8. Custom Events in Alpine.js und Web Components

Alpine.js hat Custom Events als primären Kommunikationsmechanismus integriert. Mit $dispatch('event-name', payload) dispatcht eine Alpine-Komponente ein Custom Event, das im DOM aufsteigt. Andere Alpine-Komponenten empfangen es mit @event-name.window="handler" oder @event-name="handler". Das ist idiosynkratisch für Alpine, basiert aber auf denselben Custom Events – Alpine ist nur syntaktischer Zucker über dispatchEvent(new CustomEvent(...)).

In Web Components sind Custom Events das standardkonforme Kommunikationsmodell nach außen. Ein Custom Element dispatcht Events über seine öffentliche API, Host-Seite und andere Komponenten hören darauf. Mit composed: true können Events aus dem Shadow DOM nach außen treten – ohne composed stoppt das Event an der Shadow-DOM-Grenze. Für Web Components ist es Best Practice, alle dispatched Events im JSDoc oder in einer Web Component Manifest-Datei zu dokumentieren, damit Konsumenten des Elements die verfügbaren Events kennen, ohne den Source-Code lesen zu müssen.

Kommunikationsmuster Kopplung Geeignet für Nachteil
Custom Events (DOM) Lose DOM-verbundene Komponenten Nur im Browser-Kontext
EventTarget Event-Bus Lose Globale Kommunikation, Worker Kein Debug-Tool-Support wie DevTools
Direkte Referenz Eng Parent-Child-Kommunikation Refactoring-Aufwand hoch
Globale Variable / Store Mittel Geteilter Zustand (Signal/Store) Reaktivität muss separat gebaut werden
URL / Query-Params Lose Tab-übergreifend, sharable State Nur für serialisierbaren Zustand

9. Custom Events vs. andere Kommunikationsmuster

Custom Events sind kein Allheilmittel. Sie eignen sich hervorragend für einseitige Benachrichtigungen (Broadcast), bei denen der Sender kein Ergebnis erwartet. Für Request-Response-Kommunikation – eine Komponente fragt eine andere um Daten – sind sie umständlich. Hier sind direkte Methodenaufrufe, Callbacks oder Promises die bessere Wahl. Für reaktiven gemeinsamen Zustand, bei dem mehrere Komponenten dieselben Daten lesen und Änderungen automatisch reflektiert werden, sind Signal-basierte Ansätze (wie Alpine Store, Nano Stores oder Vanilla Signals) die sauberere Lösung.

Der wichtigste Entscheidungsfaktor: Soll die sendende Komponente wissen, wer empfängt? Wenn nein, sind Custom Events richtig. Wenn ja, ist eine direkte API oder ein Store besser. Im Zusammenspiel ergibt sich ein klares Architekturmuster: Custom Events für Aktionen und Benachrichtigungen (etwas ist passiert), Stores für Zustand (was der aktuelle Wert ist), direkte Aufrufe für Services (was ich jetzt tue). Dieser Dreiklang skaliert gut von kleinen Vanilla-JS-Projekten bis zu großen Multi-Team-Architekturen.

Mironsoft

JavaScript-Architektur, Hyvä-Themes und Framework-freie Entwicklung

Entkoppelte Komponentenarchitektur für Ihr Projekt?

Wir entwerfen Custom-Event-Systeme, typsichere Event-Kataloge und lose gekoppelte Komponentenarchitekturen für Vanilla-JS, Alpine.js und Hyvä-Magento-Themes – ohne Framework-Overhead.

Architektur-Review

Bestehende Kopplung analysieren und Custom-Event-Migrations-Plan erstellen

Event-Katalog

Typsichere Custom-Event-Definitionen mit TypeScript für gesamte Codebasis

Alpine.js Integration

$dispatch und @event.window für Hyvä-Magento-Komponenten und Micro-Frontends

10. Zusammenfassung

Custom Events sind das native DOM-Werkzeug für lose gekoppelte Komponentenkommunikation. Mit new CustomEvent('name', { bubbles: true, detail: {...} }) und dispatchEvent() lassen sich Ereignisse auslösen, ohne dass Sender und Empfänger voneinander wissen müssen. Das Naming-Schema namespace:action, ein typsicherer Event-Katalog in TypeScript und das AbortController-Pattern für sauberes Listener-Cleanup sind die drei Grundpfeiler eines wartbaren Custom-Event-Systems. Für globale Kommunikation jenseits des DOM-Baums ergänzt ein leichtgewichtiger EventTarget-Bus die DOM-native Lösung.

Die Integration in Alpine.js über $dispatch und @event.window sowie die composed: true-Option für Web Components machen Custom Events zu einer universell einsetzbaren Kommunikationsinfrastruktur – von einfachen Vanillaprojekten bis zu komplexen Hyvä-Magento-Themes. Der wichtigste Designentscheid bleibt: Custom Events für Broadcast-Benachrichtigungen, Stores für reaktiven Zustand, direkte Aufrufe für Services. Wer dieses Muster konsequent anwendet, baut Frontends, die sich ohne Refactoring-Kaskade erweitern lassen.

Custom Events — Das Wichtigste auf einen Blick

Dispatchen

element.dispatchEvent(new CustomEvent('ns:action', { bubbles: true, detail: {...} })) – kein Framework nötig.

Listener-Cleanup

AbortController mit signal-Option: ein controller.abort() entfernt alle registrierten Listener. Kein manuelles Verwalten von Funktionsreferenzen.

Typsicherheit

Event-Katalog als TypeScript-Interface mit Merge in WindowEventMap. Typgeprüfter Zugriff auf event.detail und Autovervollständigung für Event-Namen.

Wann Custom Events?

Broadcast-Benachrichtigungen, bei denen Sender und Empfänger entkoppelt sein sollen. Nicht für Request-Response oder reaktiven geteilten Zustand.

11. FAQ: JavaScript Custom Events

1Was sind Custom Events?
Vom Entwickler definierte DOM-Events. Erstellt mit new CustomEvent(), dispatcht mit dispatchEvent(), empfangen mit addEventListener().
2Event vs. CustomEvent?
CustomEvent erweitert Event um detail-Property für Payload-Daten. Für Custom Events immer CustomEvent verwenden.
3Was bedeutet bubbles: true?
Event propagiert vom Ursprung nach oben bis zum document. Ermöglicht Event-Delegation mit einem zentralen Listener.
4Daten mit Custom Events übertragen?
Via detail-Property: new CustomEvent('name', { detail: { key: 'value' } }). Listener liest event.detail.key.
5Listener sauber entfernen?
AbortController: Alle mit { signal } registrieren. Ein controller.abort() entfernt alle auf einmal.
6Was ist ein Event-Bus?
const bus = new EventTarget() – gemeinsames Target für globale Kommunikation ohne DOM-Hierarchie.
7Custom Events in Alpine.js?
$dispatch('event', payload) zum Dispatchen. @event.window="handler" zum Hören. Syntaktischer Zucker über native Custom Events.
8Was ist composed: true?
Erlaubt Event, Shadow-DOM-Grenzen zu überschreiten. Notwendig für Web Components, die Events nach außen senden.
9Typsichere Custom Events mit TypeScript?
Interface mit Event-Namen und detail-Typen definieren. Typsichere dispatch()- und listen()-Hilfsfunktionen mit Generic-Constraints nutzen.
10Wann sind Custom Events nicht die richtige Wahl?
Bei Request-Response, reaktivem Zustand und synchroner Kommunikation. Dort Stores, direkte Methodenaufrufe oder Promises verwenden.