こんにちは、Another works フロントエンドエンジニアのみやぞんです。
つい最近、Reactのアップデートを行ったので React v17 での変更で学んだことについて紹介したいと思います。
##アップデート内容
- 新機能 “なし”
- 段階的なアップグレードが可能になった
公式:https://ja.reactjs.org/blog/2020/10/20/react-v17.html
破壊的変更があった部分
アップデート内容にもあった「段階的なアップグレード」を実現するための変更が「Event Delegationの変更」です。React v17 における最も大きな変更と言えます。
公式の言うEvent Delegation(イベント委譲)の変更とは、イベントハンドラが登録される方法が変わったということです。 これまではdocumentオブジェクトで全てのイベントをハンドリングしてましたが、v17 からは ReactDOM.render() の第二引数で渡された要素上でイベントをハンドリングします。
(Event Delegationでイベントのハンドリング場所を一箇所に集めることのメリットはメモリ節約やコード量の点にあるみたいです。)
以下の図は公式からの引用です。

この図にある通り、React は DOM の Event Delegation を利用して document にイベントハンドラを登録しています。 DOM の Event Delegation については W3C の図を引用します。

Event Delegation
https://www.w3.org/TR/uievents/#event-flow より
DOMのイベント実行までの流れには以下のようなフェーズがあります。
Capture Phase
イベントオブジェクトはターゲットの親たちを経由して Window からターゲットの親要素へと伝搬します。
Target Phase
イベントオブジェクトは、イベントオブジェクトのイベントターゲット(イベントがトリガーされる場所)に到着します。
Bubbling Phase
イベントオブジェクトはターゲットの親たちを逆順に伝搬し、ターゲットの親から始まり、Window で終了します。
これまでのReactではインラインで宣言したonClickなどのイベントはdocument.addListenerされます。つまり、イベントの登録をdocumentに対して行います。
デフォルトではバブリングフェーズでイベントが発火するので、この場合だと発火するタイミングは「要素をクリックしてバブリングフェーズに入ってからdocumentまでさかのぼったとき」です。
一方でReact v17では、documentではなくその下のReactをマウントしている部分(RootNode)に登録されます。なので、イベント発火のタイミングはバブリングフェーズでRootNodeに到達したときとなります。
実際のコード
これで何が変わるのかというと、Reactの外にaddEventListenerにてイベントハンドラを登録しているときの挙動です。例えばReactのRootNodeよりも上層のbodyやdocumentに対して addEventListener を使いイベントハンドラを登録している場合にReact v17へのアップデートで違いが出ます。
-
React v16
https://codepen.io/hk206/pen/wvexLwy -
React v17
https://codepen.io/hk206/pen/MWoBMYx
上記のコードは、React v16, v17でそれぞれ同じコードを書いているのですが、挙動が異なります。
このコードには body に対して登録したイベントと、ボタンにインラインで指定したイベントがあります。ボタンをクリックしたときは**e.stopPropagation()**によりバブリングを止め、そこから上層のイベントは発火しないように制御できます。
このコードで実現したかったのは、ボタンを押したときはボタンに書いた処理は走ってほしいけど、bodyをクリックしたときの処理は走らせないというものです。
React v16
React v16の場合はクリック時にどちらも走ってしまいます。なぜでしょう?
これは冒頭にも説明したとおり、インラインで書かれたイベントはdocumentオブジェクトに登録されるからで、処理としてはボタンをクリックしてからバブリングフェーズでbodyの処理がはじめに発火し、それからさかのぼってdocumentに登録されたイベントを発火します。stopPropagationを実行するときにはすでにbodyは通過しているため、思ったような挙動にはなりません。
React v17
ここではReactのRootNodeがbodyよりも下にあることを前提とします。
React v17ではちゃんと意図したとおり、ボタンクリックの場合はボタンの処理のみ、その外をクリックしたときはbodyに登録した処理が走っています。これはインラインで記述したイベントの委譲先がdocumentではなくReact RootNodeになっているからで、stopPropagationはbodyの下にあるRootNode要素で実行されるのでバブリングが止まり、bodyの処理が走ることはありません。
おわりに
これが、今回のアップデートで変更があったイベントハンドラの登録先の変更です。(Event Delegation)
実際のケースでは、例えばモーダルの外をクリックしたときに閉じる処理を実装する場合に、モーダルの最外要素にe.stopPropagation()を記述することでbodyのクリック、つまりモーダルの外側がクリックされた時にだけモーダルを閉じる仕様を簡単に実現することができます。これまでのようにモーダルの外をクリックしたのか、中をクリックしたのかという処理をわざわざ書く必要はありません。
簡単ですが、以上がReact v17へのアップデートで私が学んだEvent Delegationについてでした。
またフロントチームでLT会やプログラミング研究会という機会を活用して技術の深堀りを続けていこうと思います。