Reactのイベントハンドリングについて
はじめに
この記事はReact #2 Advent Calendar 2019 11日目の記事です。
先日JSConfに参加し、今まであまり意識せずなんとなく使ってきたReactのイベントハンドリングについて個人的に新しい発見があったので、記事にしてみます。
本記事ではReactのイベントハンドリングの仕組みについてざっくりと理解することを目的としますが、より詳しい説明や、この仕組みによって puppeteer などでテストする際にこれによってどのような技術的チャレンジが行われているかはBenjamin Gruenbaumさんの The Anatomy of a Clickという発表から知ることができるのでぜひご視聴ください。
JavaScriptのイベント伝搬について
まずJavaScriptがイベントを取り扱うのかについて見ていきます。
通常クリックイベントをあるelement以下のDOMに付与しようとすると、以下のように書くと思います。
el.addEventListener('click', listener)
このように書くことによって、 el 以下のDOMがクリックされた時、listener にそのイベントの発火が渡される形になります。このクリックからlistenerへの発火は、実はより細かく見ると、Capture Phase Target Phase Bubble Phaseというフェーズにわかれていて、上で記述した addEventListener はBubbling Phaseでこのイベントリスナーが評価されるようになっている状態です。
逆にCapture Phaseでイベントの処理を行おうとする場合、以下のようにuseCaptureオプションをtrueにします。
el.addEventListener('click', listener, true)
こちらはW3C UIEvents specificationから引用した上記フェーズの説明図となります。
まずCapture Phaseでは、Windowから順にその子要素を辿るようにイベントのターゲットである要素まで順々にイベントを処理していきます。
次にTarget Phaseでは今伝搬しているイベントをstopPropagationなどで止めるかどうかなどを判定します。
最後にBubbling Phaseでは、イベントターゲットから親の要素を辿りWindowまでイベントを伝搬していく形になります。
基本的にはBubbling
React的イベントハンドリング
ざっくりとJavaScriptにおけるイベントの仕組みについて把握したので、Reactではどのようにこのフェーズを取り扱っているのかを見ます。
Reactでは、そのイベントシステムの一部として、SyntheticEventというものが存在しています。
例えば、ReactのonClickというSyntheticEventのイベントハンドラはユーザが起こすクリックに対するイベントの処理をBubbling Phaseで行うようになっており、Capture Phaseで処理を行おうとする場合は、onClickCaptureなど、~Captureというイベントハンドラを利用することでこれが可能になっています。
また、SyntheticEventはブラウザ間のイベントの仕様の違いなどを吸収してくれています。例えば、以下のようなonMouseEnter のイベントを各種ブラウザでサポートするための以下リンク先の実装を見てみると
mouseOverとmouseOverイベントの組み合わせによってそれを判定していることがわかり、開発者があまり気にしなくても、クロスブラウザ対応をケアしてくれていることがわかるかと思います。
このような抽象化は開発者としてはとてもありがたいのですが、気をつけたいのは、これはonMouseEnterをそのままmouseOverとmouseOverイベントの組み合わせに置き換えているため、直接onMouseEnterを発火させた場合、Reactはそのイベントを処理することができないことがある可能性があるということです。テストなどを行う際に、この仕組みを知っていると、もしかしたらデバッグなどに役立つかもしれません。
まとめ
- JavaScriptのイベントの処理の仕組みには
Capture PhaseTarget PhaseBubbling Phaseという3つのフェーズが存在している - Reactは基本的には
Bubbling Phaseでイベントの処理をするようになっているが、Capture Phaseで処理するイベントハンドラも存在している - Reactは
SyntheticEventを使ってイベントを抽象化しているため、単純にイベントを同じような名前のイベントを直接発火させたとしても、反応しない場合があるので気をつける必要がある