ブラウザのカスタムイベント:CustomEventとdispatchEventの使い方
CustomEventとは
ブラウザには click や keydown などの組み込みイベントに加え、独自のイベントを定義して発火できる仕組みがあります。CustomEvent + dispatchEvent を使うことで、DOM要素間やモジュール間の通信をライブラリなしに実装できます。
基本的な使い方
// イベントを発火する const event = new CustomEvent('myEvent') document.dispatchEvent(event) // イベントを受け取る document.addEventListener('myEvent', () => { console.log('myEvent が発火した') })
CustomEvent の第1引数がイベント名です。addEventListener で同じ名前を指定すると受け取れます。
データを渡す(detail)
detail プロパティにデータを渡せます。
// 送信側 const event = new CustomEvent('userLoggedIn', { detail: { userId: 42, name: '田中太郎' }, }) document.dispatchEvent(event) // 受信側 document.addEventListener('userLoggedIn', (event: Event) => { const e = event as CustomEvent<{ userId: number; name: string }> console.log(e.detail.userId) // 42 console.log(e.detail.name) // 田中太郎 })
detail には任意のオブジェクト・配列・プリミティブ値を渡せます。
TypeScriptで型安全にする
CustomEvent はジェネリクスで detail の型を指定できます。
// カスタムイベントの型定義 type UserLoggedInEvent = CustomEvent<{ userId: number; name: string }> // ヘルパー関数でラップすると使いやすい function dispatchUserLoggedIn(userId: number, name: string) { const event = new CustomEvent<{ userId: number; name: string }>('userLoggedIn', { detail: { userId, name }, bubbles: true, }) document.dispatchEvent(event) } // 受信側 document.addEventListener('userLoggedIn', (event: Event) => { const { userId, name } = (event as UserLoggedInEvent).detail console.log(userId, name) })
オプション
| オプション | デフォルト | 説明 |
|---|---|---|
bubbles |
false |
DOMツリーをバブリングするか |
cancelable |
false |
preventDefault() で取り消せるか |
composed |
false |
Shadow DOMを超えて伝播するか |
const event = new CustomEvent('submit', { detail: { value: 'テキスト' }, bubbles: true, // 親要素にもバブリングする cancelable: true, // preventDefault() で取り消せる })
bubbles: true にすると子要素で発火したイベントを親でまとめて受け取れます。
bubbles の使い方
<div id="form-wrapper"> <input id="myInput" type="text" /> </div>
const input = document.getElementById('myInput')! const wrapper = document.getElementById('form-wrapper')! // input 要素で発火(bubbles: true) input.dispatchEvent(new CustomEvent('validated', { detail: { valid: true }, bubbles: true, })) // 親要素で受け取れる wrapper.addEventListener('validated', (event: Event) => { const e = event as CustomEvent<{ valid: boolean }> console.log('バリデーション結果:', e.detail.valid) })
EventTarget をイベントバスとして使う
DOM要素に依存せず、モジュール間通信のためにイベントバスを作れます。
// eventBus.ts export const eventBus = new EventTarget()
// モジュールA(送信側) import { eventBus } from './eventBus' eventBus.dispatchEvent( new CustomEvent('cartUpdated', { detail: { itemCount: 3 }, }) )
// モジュールB(受信側) import { eventBus } from './eventBus' eventBus.addEventListener('cartUpdated', (event: Event) => { const e = event as CustomEvent<{ itemCount: number }> console.log('カート数:', e.detail.itemCount) })
EventTarget はブラウザ標準のクラスで、new EventTarget() するだけでイベントバスになります。Node.js 14以降でも使えます。
イベントリスナーの解除
メモリリークを防ぐためにリスナーは不要になったら解除します。
function handleCartUpdated(event: Event) { const e = event as CustomEvent<{ itemCount: number }> console.log(e.detail.itemCount) } // 登録 eventBus.addEventListener('cartUpdated', handleCartUpdated) // 解除(同じ関数参照が必要) eventBus.removeEventListener('cartUpdated', handleCartUpdated)
アロー関数を変数に入れずにそのまま addEventListener に渡すと removeEventListener で解除できないため注意が必要です。
AbortSignal で一括解除する
AbortController を使うと複数のリスナーをまとめて解除できます。
const controller = new AbortController() const { signal } = controller eventBus.addEventListener('cartUpdated', handleCart, { signal }) eventBus.addEventListener('userLoggedIn', handleUser, { signal }) // まとめて解除 controller.abort()
コンポーネントのアンマウント時やページ離脱時に一括解除するのに便利です。
cancelable と preventDefault
cancelable: true のイベントは受信側で preventDefault() を呼んで取り消せます。
// 送信側 const event = new CustomEvent('beforeDelete', { detail: { id: 1 }, cancelable: true, }) const cancelled = !document.dispatchEvent(event) // 取り消されていなければ削除処理を実行 if (!cancelled) { deleteItem(1) } // 受信側(取り消し) document.addEventListener('beforeDelete', (event: Event) => { if (!confirm('削除しますか?')) { event.preventDefault() // 削除をキャンセル } })
dispatchEvent はイベントが取り消されなければ true、preventDefault() で取り消されると false を返します。
まとめ
| やること | コード |
|---|---|
| カスタムイベントを発火 | element.dispatchEvent(new CustomEvent('名前')) |
| データを渡す | new CustomEvent('名前', { detail: { ... } }) |
| データを受け取る | (event as CustomEvent<型>).detail |
| DOMをまたいで通知 | bubbles: true |
| モジュール間通信 | new EventTarget() でイベントバスを作る |
| リスナーの解除 | removeEventListener または AbortController |
detailにデータを入れて型はCustomEvent<T>で指定する- モジュール間通信は
new EventTarget()をシングルトンとして使う - リスナーの解除は
AbortControllerでまとめて管理するとシンプル
ブラウザイベントの伝播(キャプチャ・バブリング・stopPropagation)については「ブラウザイベント入門:キーボード・マウス・フォーカスの発火順序」を参照してください。