React 17 アップデートのポイント - useEffect のクリーンアップ関数の実行タイミングの変更
(このスクリーンショットは React 公式ブログより引用)
React 17 はすでに RC 段階に入った。公式ブログの声明によると、今回のリリースには重大な更新や新機能はないとのことだが、記事の中にはいくつか興味深い点が見つかるので、ここでまとめて紹介する。
この記事ではすべてのアップデートを逐一解説するのではなく、記事の中で特に注目すべきポイントを記録していく。
イベント委譲(Event Delegation)のノード変更
React では、<button onClick={}> のようなイベントリスナーを使用する場合、React はすべてのイベントリスナーをノード自体ではなく document に配置する。この手法はイベント委譲(Event Delegation)と呼ばれ、大量のイベントリスナーによるパフォーマンスの問題を防ぐメリットがある。React 17 以降では、リスナーは document ではなく root 要素に変更される。React の制御下に置かれるため、ツイートで言及されていた Replay Event などの新機能を後から追加しやすくなる。
Event Pooling 機構の削除
パフォーマンス向上のため、React 17 までは Event Pooling 機構、つまり独自に実装した Synthetic Event が使われていた。しかし公式によると、モダンブラウザにおいてそれによるパフォーマンスの改善は実際には限定的であり、かえって開発者に混乱を招いていたという。たとえば、e.target を引数として他のコンポーネントに渡そうとすると、なぜか null になっていることに気づく。これは React がパフォーマンス向上のために行った設計によるものだ。正しく使うためには、React が中身をリセットしないよう追加で e.persist() を呼び出す必要があった。
React 17 以降では Event Pooling の仕組みが完全に削除されたため、余計に e.persist() を呼び出す必要もなくなった。素晴らしい。一部の記事やチュートリアルでは React の Event Pooling 設計が強調されていたが、今後はその説明も変える必要があるかもしれない。
要チェック:useEffect のクリーンアップ関数の実行タイミング変更
これがおそらく今回のバージョンアップで最も重要な変更点だ。
useEffect を使用する際、大半の effect は画面の描画更新とは関係がないため、React は画面の描画更新後に effect を実行する。
全体的なフローは以下のようになる:
コンポーネントの更新 → DOM の対応する変更 → 画面の描画更新 → effect の実行
これにより、effect 内の処理の計算量が大きすぎて DOM の描画に影響を与えるのを防いでいる。例えば:
const App = () => {
useEffect(() => {
longlongTask(); // 我會跑很久哦
console.log('Hello World');
})
return <Component />
}
上述のフローのおかげで、React は画面の更新後に他の effect を実行することを保証し、ユーザー体験が損なわれるのを防いでいる。
しかし、画面のチラつきを防ぐために画面の更新前に effect を実行したいケースもあるだろう。その場合は useLayoutEffect で代用できる。
ここまではすべて React 16 の想定された動作のおさらいだ。次に話すのはクリーンアップ(clean up)の部分についてだ。
React 16 では、useEffect の effect 自体は画面更新後に実行されていたが、クリーンアップ関数は画面更新の前に実行されていた。通常これは大した影響はなく、ほとんどのクリーンアップ関数は単に unsubscribe したり、リスナーを解除したり、API をキャンセルしたりするだけだ。しかし、クリーンアップ関数が時間を食いすぎる場合、画面の描画更新に影響が出てしまう。
例えばこのように書いたとする:
const App = () => {
useEffect(() => {
longlongTask(); // 我會跑很久哦,但沒關係 react 會在畫面更新後才執行
console.log('Hello World');
return () => {
longlongTask(); // 畫面會卡住直到 longlongTask() 結束(在 React 16 當中)
}
})
return <Component />
}
React 16 では、クリーンアップの実行タイミングは以下のようになっていた(コンポーネント更新時):
コンポーネントの更新 → DOM の対応する変更 → クリーンアップ関数の実行 → 画面の描画更新 → effect の実行
そして React 17 以降では、クリーンアップのタイミングも画面更新後へと変更された(コンポーネント更新時):
コンポーネントの更新 → DOM の対応する変更 → 画面の描画更新 → クリーンアップ関数の実行 → effect の実行
コンポーネントがアンマウント(unmount)される場合:
React 16:コンポーネントの更新 → クリーンアップ関数の実行 → DOM の対応する変更 → 画面の描画更新
React 17:コンポーネントの更新 → DOM の対応する変更 → 画面の描画更新 → クリーンアップ関数の実行
クリーンアップ関数は useEffect の順序通りに実行される
React 17 executes the cleanup functions in the same order as the effects, according to their position in the tree. Previously, this order was occasionally different.
どのような状況でクリーンアップ関数の実行順序に依存関係が必要になるのかは分からないが、React 16 ではクリーンアップが実行されるタイミングの順序が予期せぬものになることがたまにあったようだ。
関連記事
- Three.js で僕の部屋を表現する 僕が React Three Fiber を使って実際の自分の部屋をブラウザ上に再現し、実世界のオブジェクトを目録に見立て、空間の記憶を通してここ数年の生活と仕事について語った話。
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。