Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

エフェクトイベント関数はuseEffectのdepsに"入れてはならない"

depsに入れなくて良い、ではなく、入れてはならない です。

エフェクトイベント関数をdepsに入れるとレンダリング毎にエフェクトが走る

useEffectEventは レンダリングごとに新しい関数を返します (検証用デモ)。 そのため、useEffect の deps にエフェクトイベントを含めると毎回エフェクトが実行されます(時にはこれによって 無限ループが起きます )

const effectEventFn = useEffectEvent(callback); // レンダリングのたびに新しい関数が返される

useEffect(() => {
  return listen(effectEventFn);
}, [effectEventFn]); // ❌ effectEventFn が入っているので毎回実行される!!

なので、depsにeffectEventFnを入れてはいけません。

useEffect(() => {
  return listen(effectEventFn);
}, []); // ⭕️ depsにエフェクトイベント関数が入ってない

なぜこのような実装になったのでしょうか?

背景: ランタイムでのアサーションとして機能させるため

この意思決定の背景はこのPRが参考になります。

[useEvent] Non-stable function identity by poteto · Pull Request #25473 · facebook/react · GitHub

これによると、「useEffectEventがstableであることに依存したエフェクトを再実行することで、depsにエフェクトイベントを入れてはいけないとユーザーに学んでもらうため 」にunstableに変更したようです。

補足説明: なぜunstableであるべきなのか?

上の説明だけではわかりづらいので、いくらか補足します。

まず、useEffectEventを stableで定義する (初回に作成した関数が常に返される) と、useEffectのdepsに入れても入れなくても同じ挙動になります。 実際、stableで定義されているpolyfillの方は、depsに入れても動きます。

useEffect(() => {
  return listen(effectEventFn);
}, [effectEventFn]); // effectEventFnがstableだったらこれでも問題ない

これに対し、useEffectEventを unstableで定義する (レンダリング毎に新しい関数が返される) と、以下のようになります。

  • useEffectのdepsに入れると毎回エフェクトが走る
  • depsに入れないと更新されてもスキップされる

reactのメンタルモデル的には、useEffectのdepsに入れないで欲しいようです。 そのため、"ランタイムアサーション"が追加されている方が良いという結論になったようです。

まとめ: useEffectのdepsは自分で「選ぶ」たぐいの物ではない

今までのuseEffectのdepsは以下の2種類でした。

  • depsに入れても影響しないもの(refなど)
  • depsに入れるとエフェクトがスキップされるようになるもの

19.2でエフェクトイベント関数が加わったことで、さらに以下が加わりました。

  • depsに入れると不要なエフェクトが走るようになるもの

以前からドキュメントにて useEffectのdepsは 自分で「選ぶ」たぐいの物ではない *1と明言されています。エフェクトイベントもこれに照らし合せた実装が行われたということです。 エフェクトイベントの追加がされた今、改めてこのことを肝に銘じておきましょう。