JavaScript カスタムイベント開発者ガイド
クリーンなコンポーネントアーキテクチャを構築したものの、密結合を避けながらコンポーネント間で通信する必要が出てきました。ネイティブのDOMイベントはユーザーインタラクションの処理には優れていますが、アプリケーション固有のシグナルについてはどうでしょうか?JavaScript カスタムイベントがこの問題をエレガントに解決します。
このガイドでは、カスタムイベントの作成とディスパッチ、CustomEvent の detail ペイロードを通じた構造化データの受け渡し、軽量なイベントバスとしての EventTarget の使用方法について説明します。また、Web Components におけるカスタムイベントの動作や、Shadow DOM カスタムイベントが境界を越えて伝播する仕組みについても学びます。
重要なポイント
CustomEventコンストラクタとdetailプロパティを使用して、イベントを通じて構造化データを渡すEventTargetインターフェースは単独で動作し、DOM要素なしで軽量な pub/sub パターンを実現できる- DOMツリーを上方向に伝播させるイベントには
bubbles: trueを設定する - Web Components で Shadow DOM の境界を越えてイベントを伝播させるには
composed: trueを使用する
カスタムイベントの作成とディスパッチ
CustomEvent コンストラクタは、カスタムイベントを作成する現代的な方法です。initCustomEvent は忘れてください—それはモダンブラウザでは不要なレガシーAPIです。
const event = new CustomEvent('user-login', {
detail: { userId: 123, timestamp: Date.now() },
bubbles: true,
cancelable: true
})
document.querySelector('#app').dispatchEvent(event)
ここで重要なオプションは3つです:
- detail: 構造化されたペイロード(シリアライズ可能な任意のデータ)
- bubbles: イベントがDOMツリーを上方向に伝播するかどうか
- cancelable: リスナーが
preventDefault()を呼び出せるかどうか
リスナーの設定はネイティブイベントと全く同じです:
document.querySelector('#app').addEventListener('user-login', (e) => {
console.log(e.detail.userId) // 123
})
CustomEvent の detail ペイロード
detail プロパティは、基本的な Event コンストラクタに対してカスタムイベントが優れている点です。技術的には Event オブジェクトに作成後に任意のプロパティを割り当てることもできますが、detail はデータ用の専用の、競合のない名前空間を提供します。
const cartEvent = new CustomEvent('cart-updated', {
detail: {
items: [{ id: 1, qty: 2 }, { id: 3, qty: 1 }],
total: 59.99,
currency: 'USD'
}
})
ハンドラーは event.detail を通じてこれにアクセスします—クリーンで予測可能です。
イベントバスとしての DOM EventTarget
イベントを使用するためにDOM要素は必要ありません。EventTarget インターフェースは単独で動作するため、軽量な pub/sub メカニズムに最適です:
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!' })
この EventTarget ベースのイベントバスパターンは、外部依存なしでコンポーネントを疎結合に保ちます。TypeScript ユーザーの場合、ジェネリクスを使用して detail ペイロードに型を付けることができます: CustomEvent<{ message: string }>。
注意: Node.js では EventTarget は存在しますが、CustomEvent は Node 19+ でのみグローバルとして利用可能です。Node では EventEmitter がより一般的なパターンです。
Discover how at OpenReplay.com.
Web Components におけるカスタムイベント
Web Components は外部への通信にカスタムイベントを大きく依存しています。コンポーネントがイベントをディスパッチし、親コードがリッスンします:
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)
親はコンポーネントの内部を知らなくてもリッスンできます:
document.querySelector('user-card').addEventListener('user-selected', (e) => {
loadUserProfile(e.detail.id)
})
Shadow DOM カスタムイベントと composed オプション
Shadow DOM カスタムイベントは composed オプションに基づいて異なる動作をします:
composed: false(デフォルト): イベントは Shadow Root の境界で停止します。内部実装は隠蔽されたままです。composed: true: イベントは Shadow の境界を越えて Light DOM を通じてバブリングします。
// Shadow DOM 内部
this.shadowRoot.querySelector('button').dispatchEvent(
new CustomEvent('internal-action', {
bubbles: true,
composed: true // Shadow の境界を越える
})
)
イベントが Shadow の境界を越えると、event.target はホスト要素にリターゲットされます—外部のリスナーはコンポーネントを見ますが、その内部構造は見えません。
外部コードが処理すべきイベントには composed: true を使用してください。内部コンポーネント通信には composed: false を維持してください。
event.isTrusted について簡単に触れておくと、このプロパティはブラウザ(ユーザーアクション)とスクリプトのどちらがイベントを生成したかを示します。これは情報提供用であり、セキュリティメカニズムではありません—アクセス制御に依存しないでください。
まとめ
JavaScript カスタムイベントは、疎結合でイベント駆動型のアーキテクチャを構築するためのフレームワーク非依存の方法を提供します。データには構造化された detail ペイロードを持つ CustomEvent を使用し、スタンドアロンのイベントバスとして EventTarget を活用し、特に Shadow DOM の境界を越えた伝播を制御する bubbles と composed の動作を理解してください。これらのパターンは、シンプルなコンポーネント通信から複雑なマイクロフロントエンドアーキテクチャまでスケールします。
よくある質問
主な違いは detail プロパティです。CustomEvent には構造化データを渡すための専用の detail プロパティが含まれていますが、基本的な Event コンストラクタにはありません。Event オブジェクトに作成後にプロパティを追加することもできますが、detail を持つ CustomEvent を使用する方がクリーンで、既存のイベントプロパティとの名前の競合を回避できます。
はい、ただし注意点があります。React は JSX でカスタムイベントリスナーをネイティブにサポートしていないため、ref を使用して addEventListener で手動でリスナーをアタッチする必要があります。Vue はそのイベントシステムを通じてカスタムイベントをより適切に処理します。カスタムイベントは、フレームワークコンポーネントとバニラ Web Components やフレームワーク非依存のコード間で通信する際に最も効果的です。
いいえ。親要素がイベントをキャッチする必要がある場合にのみ bubbles を true に設定してください。同じ要素でディスパッチとリッスンを行う直接通信の場合、バブリングは不要です。過度なバブリングは意図しないリスナーがイベントをキャッチする原因となる可能性があるため、伝播については意図的に設定してください。
addEventListener に渡したのと全く同じ関数参照を使用して removeEventListener を使用してください。匿名関数は渡す参照がないため削除できません。ハンドラー関数を変数に保存するか、よりクリーンなクリーンアップのために signal オプションで AbortController パターンを使用してください。
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.