先日、React v17リリース候補がリリースされましたね!
前回のメジャーリリースからなんと2.5年もかかったようです。
ほとんどが公式の意訳になりますが、何が変わったのか重要そうなところをピックアップして自分メモとしてまとめておきます。
全てを網羅しているわけではないので、より詳しく知りたい方は下記の公式リリースノートを参照ください。
公式リリースノート
公式リリースノート日本語版
新しい機能はないよ
React開発者は現在新しい機能追加に向けて取り組んでいますが、今回のv17には実は新機能は追加されていません。
v17は言わば、これからリリース予定の大規模アップデートのための踏み石になっています。
段階的アップデート
今までのアップデートには必ず破壊的変更が含まれていました。v15 -> v16の破壊的変更は皆さんの記憶にも新しいかと思います。そのような破壊的変更は、メンテされてないコードは特に、アップデート作業は困難を極めます。
1つのページに複数のバージョンのReactを置く、という対処方法もありますが、元来のReactではその方法ではイベントがうまく起動しないという問題がありました。
v17ではそのような問題が解消されています!🎉
これがreact v17の一番の目玉となっています。
これからのアップデートでも、各react毎にアップデートし、古い部分は残す、という作業が可能になるように設計されていくそうです。
先ほど新しい機能の追加はない、と言いましたが、破壊的な変更がないわけではありません。
上記のイベント問題を解消するために、特にイベント部分が今までと違う実装となっています。
実際にはFacebookではアップデートのために変更しなければならなかったコンポーネントは
100,000以上のコンポーネントのなかで20個ほどしかなかったので、ほとんどのアプリでは大きなトラブルはないだろう、とのことです。
段階的アップグレードをlazy-loadでどのようにするか、のdemoを公式が用意してくれています。
イベントの委任に関する変更
v17でのイベントの内部変更について見てみましょう。
下記はよくあるReactでのイベント登録です。
<button onClick={handleClick}>
これをバニラJSに直すと下記になります。
myButton.addEventListener('click', handleClick);
上記では、reactでボタンに登録したclickイベントが、そのDOMのイベントとして登録されています。
しかしReactのイベントの中には、登録すると宣言したDOM以外にイベントが登録される場合があります。
どこに登録するかというと、**document
に一括で登録されます。
これはイベントの委任(Event Delegation)**と呼ばれる一般的なテクニックです。
イベントの委任について詳しくはこちら。
イベントの委任をすることでパフォーマンス改善にもなり、イベントの再生を行うことができるなど、色々な機能が使えるようになります。
しかし、この仕組みは段階的アップデートを行う際問題となってしまいます。
もし1ページに複数の異なるバージョンのReactを設置しようとすると、どちらのイベントもdocument
に登録されてしまうため、e. stopPropagation()
が上手く動かなくなります。
ちなみにイベントの伝播について復習したい方は、下記の記事がおすすめです。
バブリング と キャプチャリング
上記問題は実際にissueに上がっているので、理解を進めるためにリリースノートを一旦離れ、issueの内容を見てみます。
Attach event listeners at the root of the tree instead of document #8117
To understand how this solves the problem, let's assume that we have OuterComponent which is running React version A and InnerComponent that is running version 2. The inner component attaches the event listener at the top of the inner tree using bubble phase and the outer component at the root of the outer component.
When there's a click event on the innerComponent, the inner version of React will be notified first because it's deeper in the tree, which will dispatch the event through the innerComponent hierarchy and eventually something will call React e.stopPropagation(), which will call the DOM e.stopPropagation(), so that the outer version of React will never be notified.
意訳: この問題を理解するために、OuterComponentというreactのバージョンその1と、InnterComponentというreactのバージョンその2があると仮定します。InnterComponentはバブルフェーズでinnterのトップにイベントを付与し、OuterComponentも同様にバブルフェーズでouterのトップにイベントを付与します。
innterComponentをクリックした際、ツリーの奥側にあるinnerのバージョンその2のreactがouterより先にそれを感知し、イベントをディスパッチします。そしてe.stopPropagation()が呼ばれます。
その為、外側のバージョンその1のreactはイベントを感知することができません。
この問題を解消する為v17ではdocumentにイベントを委任せず、reactがrenderされるroot DOMにイベントを委任するようになりました。
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
// 上記の場合、 id=root のDOMにイベントが委任される
※公式リリースノートから拝借した画像です。
この変更はreactの段階的アップデートのみでなく、他の技術と組み合わせてreactを使う際にも有効です。
外側のコードをjqueryで書き、内側でreactを使う場合でも、e.stopPropagation()
に悩まされずに済みます。
アップデートの際気をつけること
document.addEventListener(...)
を記述している際は要注意です。
上記イベントを登録し、reactのイベント内でe.stopPropagation()
を呼び出していても今までのreactバージョンではdocumentのカスタムイベントを走らせることができました。
しかし、v17ではe.stopPropagation()
により伝播が止まってしまいます。リクエスト通りになるということですね。
document.addEventListener('click', function() {
// reactのコンポーネントが e.stopPropagation()を読んでいたらここは実行されない
});
下記のようにキャプチャリングをtrue
にすることで調整しましょう。
これでバブリング時ではなくキャプチャリング時に発火するので、e.stopPropagation()
の影響を受けずにすみます。
document.addEventListener('click', function() {
}, { capture: true });
その他破壊的変更
イベントのシステムに関して若干の変更があります。
-
onScroll
がバブリングしなくなりました。 -
onFocus
とonBlur
はネイティブのfocusin
とfocusout
を内部で使用するようになりました。つまり、JS本来の動きに近くなったということですね。 -
onClickCapture
といったキャプチャフレーズイベントは、実際のブラウザのキャプチャフレーズのリスナーとなりました。
上記で注意すべきなのは、onFocus
は内部でfocus
を使用していたものをfocusin
に変更していますが、依然としてバブリングするという点です。
Effect クリーンアップのタイミング
useEffectのクリーンアップのタイミングに一貫性を持たせたとのことです。
useEffect(() => {
return () => {
// クリーンアップ
};
});
通常、クリーンアップ関数を走らせる場合、画面遷移を遅らせる必要はないので、reactでは画面更新後すぐに非同期処理としてクリーンアップ関数を走らせます。(同期処理が必要な場合はuseLayoutEffect
を使うべきです。)
しかし、v16のeffectのクリーンアップ関数では同期的に実行されていました。
これは大きなアプリでの画面遷移ではパフォーマンス面に置いて致命的です。
v17のuseEffectのクリーンアップは非同期処理として実行されます。コンポーネントがアンマウントされ、画面が再描画された後に実行されます。
加えて、react17では、クリーンアップ関数の実行順が違うことがありましたが、
ツリー内の順番通りに実行されるとのことです。
潜在的な問題
useEffect(() => {
someRef.current.someSetupMethod();
return () => {
someRef.current.someCleanupMethod();
};
});
上記のsomeRef.current
はimmutableなので、クリーンアップ実行時にはnull
になっている場合があります。
useEffect(() => {
const instance = someRef.current;
instance.someSetupMethod();
return () => {
instance.someCleanupMethod();
};
});
上記のように変数に割り当てておきましょう。
undefined返却で常にエラーとなるようになった
reactではクラスと関数のコンポーネントに関してundefinedとなっていないかのチェックをしています。
function Button() {
return // エラー
}
これは値を返し忘れていることを防止する意図があります。
function Button() {
// return書き忘れでundefinedが返却値になってしまっている
<button />;
}
しかし、v16ではforwardRef
や memo
といった、同じチェックを必要とするコンポーネントに上記のエラーチェックが含まれていませんでした。
v17ではforwardRef
memo
でもundefined
が返されるとエラーが出るように変更されています。
(意図的に何も返したくないときはnull
を返してね!)