Closed6

React 18のuseEffectの変更点について

HaruHaru

https://beta.reactjs.org/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed

React 18 のstrictモードでは、開発中に2回useEffectが発火する

useEffect(() => {
  // strictモードでの開発中には、2回コールされる
  // 本番環境では仕様通り1回のみ
  console.log('Hello')
}, [])

React 18以前のstrictモードでも、関数コンポーネントは2回呼び出されている。

今回はその挙動に加えて、useEffectも2回発火されるように変更。

処理としては、コンポーネントのマウント、アンマウント、再マウントが意図的に走る。
そのため結果としてuseEffectも2回コールされる。

このようなバグは、大規模な手動テストなしで見逃すことは容易である。このようなバグを素早く発見するために、開発中のReactでは、すべてのコンポーネントを最初にマウントした直後に一度だけ再マウントしています。接続中...」というログを2回見ることで、本当の問題に気づくことができます。コンポーネントがアンマウントされたときに、あなたのコードが接続を閉じていないのです。

開発時の処理の詳細はこちら

  • React がコンポーネントをマウント
    • レイアウト副作用を作成
    • 副作用を作成
  • マウントされたコンポーネント内で副作用の破棄をシミュレート
    • レイアウト副作用を破棄
    • 副作用を破棄
  • マウントされたコンポーネント内で以前の state を復元し副作用の再生成をシミュレート
    • レイアウト副作用を作成
    • 副作用の作成用コードの実行
HaruHaru

React 18 未満のuseEffect の挙動。
本番環境も開発環境も同じ挙動。

useEffect(() => {
  // レンダリング後、毎回発火
  console.log('Hello')
});

useEffect(() => {
  // マウント時(コンポーネント初期化時)に一回のみ発火
  console.log('Hello')
}, []);

useEffect(() => {
  // isEnabled変更時のみ発火

  // 何かしらのisEnabledの処理...

  console.log('Hello')
}, [props.isEnabled]);

HaruHaru

複数回の発火のために、必ずクリーンアップしよう

現時点では、あくまでテストしやすいようにstrictモードでの開発時に2回発火される処理になっている。

2回と言わず、複数回発火されても何も問題が出ないように、
useEffectのクリーンアップをしっかりしよう、という処置。

チャットの接続処理をしたならば、
接続解除のクリーンアップの関数をreturnする

useEffect(() => {
  const connection = createConnection();
  connection.connect();
  return () => {
    connection.disconnect();
  };
}, []);

この場合何回発火されても、以前の接続が残ることはなく、
常にチャットへの接続は一本のみとなる

HaruHaru

ログ送信のような、クリーンアップができない処理に関して

ページを開いた際に、ログを一回だけ送りたい場合。

通常通り、下記の処理で問題ない。

useEffect(() => {
  logVisit(url);
}, [url]);

ただ開発中には2回コールされている。

Q: ではどういう対応をするのがいいのだろうか?
A: なにもしなくてもよい

開発中のみ2回呼ばれるだけなので、まずそれを許容する。
開発時のログ対応のために、複雑なコードにならないようにする方が開発上は健全なため。

https://beta.reactjs.org/learn/synchronizing-with-effects#sending-analytics

HaruHaru

将来的な仕様変更

https://ja.reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-strict-mode

現状本番環境ではマウント時に一回のみの挙動。

useEffect(() => {
  console.log('Hello')
}, [])

ただマウント後も将来的には副作用が複数回発火されることがあるため、
Strictモードでは問題に気づきやすくなるようにしますね、という仕様変更。

将来的に、React が state を保ったままで UI の一部分を追加・削除できるような機能を導入したいと考えています。例えば、ユーザがタブを切り替えて画面を離れてから戻ってきた場合に、React が以前の画面をすぐに表示できるようにしたいのです。これを可能にするため、React は同じ state を使用してツリーをアンマウント・再マウントします。
この機能により、React の標準状態でのパフォーマンスが向上しますが、コンポーネントは副作用が何度も登録されたり破棄されたりすることに対して耐性を持つことが必要になります。ほとんどの副作用は何の変更もなく動作しますが、一部の副作用は一度しか登録・破棄されないものと想定しています。
この問題に気付きやすくするために、React 18 は strict モードに新しい開発時専用のチェックを導入します。この新しいチェックは、コンポーネントが初めてマウントされるたびに、すべてのコンポーネントを自動的にアンマウント・再マウントし、かつ 2 回目のマウントで以前の state を復元します。

HaruHaru

将来的にはどうなるの?

Strictモードはあくまでチェック用の処理であり、
今後の仕様変更でマウント時に2回発火するようになるわけではない。

あくまでマウント時には1回のみの発火になるが、
今後別の状態が追加される。

表示/非表示の新たな概念として、オフスクリーン(offscreen) 機能が追加される。
オフスクリーン(offscreen) 機能についてはこちら

https://zenn.dev/takeharu/scraps/25227bbb2506b3

このスクラップは2023/01/23にクローズされました