Reactとブラウザのイベントシステムの違い

2024/03/03に公開

はじめに

Reactでイベントに対する処理を実装することはよくあるかと思います。Reactのイベントシステムとブラウザのイベントシステムの違いについて正しく理解していなかったため、今回はその違いについてまとめました。

そもそもイベントとは

例えば、ウェブサイトにおいてボタンがクリックされたときや、テキストフィールドに入力があった際に発生するもの、それがイベントです。以下は、ウェブ上で遭遇する一般的なイベントの一覧です。
https://developer.mozilla.org/ja/docs/Web/Events
これらイベントの発生に対して何らかの反応(アクション)を実行することで、ユーザーとのやりとりや動的なサイトを実現しています。
また、イベントの発生に対してアクションを行う際は、イベントリスナーイベントハンドラー を使います。イベントリスナーはイベントが発生した際に実行される関数を登録する仕組みを指します。イベントハンドラーはイベントが発生した際に実行される関数のことを指します。

ブラウザのイベントシステム

1. 処理の登録方法

以下の二通りがあります。

  • addEventListnerを使う方法
el.addEventListener("click", someFunction);
  • onclickなどのプロパティに対してイベントハンドラーを渡す方法
<button onclick="alert('button クリック')">クリック</button>

2. イベントオブジェクト

イベントが発生すると、イベントオブジェクトが生成されます。以下のように、イベントハンドラーの中で取得することができ、中にはイベントの種類やイベントが発生した要素の情報などが含まれています。

<button onclick="(e) => console.log(e.target)">クリック</button>

3. イベントの伝搬

  • イベントが発生した要素からその要素の親や祖先要素に、イベントが伝わるプロセスを指します。
  • 例えば、以下のコードのbuttonをクリックした場合、buttonの親であるdivにもクリックイベントが伝搬します。そのための順でalertが呼び出されます。
<div onclick="alert('親')">
  <button onClick="alert('子')">クリック</button>
</div>
  • 上記の例では子から親要素にイベントが伝搬しているように見えますが、実際には下図のように3つのフェーズを通してイベントが伝播しています。

    Graphical representation of an event dispatched in a DOM tree using the DOM event flow

1. キャプチャーフェーズ (Capture Phase)

  • イベントが発生した際、DOMツリーの一番外の要素(window)から、イベントが発生した要素(td)の親要素(tr)までイベントが伝搬します。
  • 実際には3.バブリングフェーズでイベント発生に対する処理を実行するため、ほとんど使うケースはありません。
  • このフェーズでイベントの発生に対して処理を実行したい場合、イベントリスナーにて以下の通り実装すれば可能です。
el.addEventListener(someFunction, { capture: true })
// または
el.addEventListener(someFunction, true)

2. ターゲットフェーズ (Target Phase)

  • ターゲット要素の親要素(tr)からターゲット要素(td)にイベントが伝搬するフェーズです。
  • このフェーズではハンドラは呼び出されませんが、1.キャプチャフェーズ3.バブリングフェーズ の両方のハンドラがこのフェーズで呼び出されます。

3. バブリングフェーズ (Bubbling Phase)

  • ターゲット要素(td)から、DOMツリーの一番外の要素(window)までイベントが伝播します。
  • 登録されたイベントリスナーやイベントハンドラーは基本このフェーズで呼び出されます。
  • 基本的に全てのイベントはバブリングしますが、focusblurなど一部イベントは例外的にバブリングしません。

Reactのイベントシステム

1. 処理の登録方法

  • JSXのonClickonChangeなどの特定のイベントプロパティに対して、イベントハンドラーを渡す形で登録します。
<button onClick={someFunction} />
  • JSXのプロパティに渡されたイベントハンドラーは、内部でReactツリー上のアプリがマウントされるルート要素に対してイベントリスナーとして登録されています。
  • これは、イベントの移譲(Event Delegation) と呼ばれる手法で、この内部的な実装によりパフォーマンスが最適化されています。
イベントの移譲

共通の親要素に対してひとつのイベントリスナーもしくはハンドラを設定することで、子要素で発生するイベントを一つの親要素で検知して処理するイベントのハンドリングパターンです。

<ul onclick="(e)=> console.log(e.target.id)">
    <li id="1">A</li>
    <li id="2">B</li>
    <li id="3">C</li>
</ul>

2. イベントオブジェクト

  • Reactのイベントオブジェクトはブラウザのイベントオブジェクトと違い 合成イベント(SyntheticEvent) と呼ばれます。
  • 合成イベントは、各ブラウザのネイティブイベントをラップしたイベントオブジェクトで、ブラウザ間のイベントの違いを吸収して動作が同じになるように実装されています。
  • 以下は主なコンポーネントがサポートする合成イベントの一覧です。

https://ja.react.dev/reference/react-dom/components/common

3. イベントの伝搬

  • DOMツリーではなくReactツリーの構造に従って伝搬します。
    例えばcreatePortalを使って実装されたportal要素は、DOMツリーとReactツリーで位置が違いますが、Reactツリー内での親子関係に従ってイベントが伝搬します。
  • イベントに対する処理は常にバブリングフェーズで実行されます。
  • scrollイベントを除き、基本的に全てのイベントがバブリングします。
    例えば、合成イベントのfocusblurは、ブラウザのイベントと違い、内部的にfocusinfocusoutイベントに置き換えられて実装されているためバブリングします。
  • キャプチャーフェーズでイベントに対する処理を実行したい場合、on〇〇Captureというプロパティにハンドラを登録れば可能です。
// クリックイベント伝搬のキャプチャフェーズに実行
<button onClickCapture={someFunction}>クリック</button>

まとめ

Reactのイベントシステムはブラウザのイベントシステムと比較し、使いやすく最適化されていることが理解できました。

参考

https://www.w3.org/TR/uievents/#event-flow
https://ja.react.dev/reference/react-dom/createPortal
https://tech.smarthr.jp/entry/2020/11/02/135954
https://stackoverflow.com/questions/34926910/onfocus-bubble-in-react

Discussion