useEffect()の第2引数の差分検出はdeepではない

内容はタイトルで尽きてます。 気になって調べたのでメモを残しておきます。

背景

  • React HooksのuseEffect()第2引数に渡す配列は、「この副作用が依存する値」を表す。
    • つまり、指定した配列に含まれる値が変化したときのみ、第1引数の関数が実行されるようにできる
  • では、変化の検知はどういう方式なのか?
    • 具体的には、配列やオブジェクトを渡した場合にdeepな比較をしてくれるのか?

結論

  • Deepな比較はしてくれない。
    • 値が全く同じでも、別オブジェクトなら変更検知される。逆もまた然り。
  • とはいえ、通常はprops, stateくらいしか指定しないだろうから、同じオブジェクトだけど値が変わった、みたいな状況はほぼ気にしなくて良さそうではある

関連記事

実験

useEffect()の第2引数に

  1. 数値をプロパティに持つオブジェクト
  2. 数値を要素に持つ配列
  3. プリミティブ数値

を渡し、各数値をインクリメントしつつ1秒おきにrenderする。 直接innerTextを書き換えるような副作用を仕掛けておき、差分が検出されるか調べた。

実際、オブジェクト・配列については中身の値が変わっていても副作用が実行されなかった。

コード確認

  • React v16.13.1 を読んだ
  • Reactのコードをちゃんと追うのは時間がかかりそうなので断念した
    • 当たりを付けた上で、DevToolsでデバッグして実際に該当コードが実行されていることを確認した

useEffect()の実装

  • React内部では、ReactCurrentDispatcher.currentというグローバル変数を状況に応じて差し替えながら処理しているらしい
  • それを元に追いかけると、useEffect()の実体がupdateEffectImpl()という関数であることがわかる
  • 差分検知はareHookInputsEqual()がしている
    • 第2引数(配列)の各要素のbefore/afterの値をis()関数で比較している
      • このへん
      • is()関数はObject.is()Polyfill、ざっくり言うと===による同一性比較だが、+0-0は異なるがNumber.NaNNaNは等しいと判定する。
    • 各要素を再帰的に見るような処理は存在しないことから、deepな比較はしないことがわかる