ブラウザのカスタムイベント:CustomEventとdispatchEventの使い方

スポンサーリンク

ブラウザのカスタムイベント:CustomEventとdispatchEventの使い方

CustomEventとは

ブラウザには clickkeydown などの組み込みイベントに加え、独自のイベントを定義して発火できる仕組みがあります。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 はイベントが取り消されなければ truepreventDefault() で取り消されると 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)については「ブラウザイベント入門:キーボード・マウス・フォーカスの発火順序」を参照してください。