· 4分で読了

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 ではクリーンアップが実行されるタイミングの順序が予期せぬものになることがたまにあったようだ。

関連記事

他のトピックを探索