useEffectを使う場合、

  • 引数なしのときはレンダリングのたびに実行
  • 引数があるときは指定された値が変わったとき以外はスキップ(つまりマウント時は値が挿入されたときであるため必ず実行されるし、空配列の場合は変わる値がないためマウント時のみに実行される)

このふたつだけを理解していれば、使うことはできる。
ただし複雑なアプリケーションを構築する場合、任意のタイミングでエフェクトを実行したいときに、このシンプルな構造が管理しにくくなるだろう。

とくに初心者が最初に躓くであろうStrict Mode時に二回実行されるような挙動は最初は設計意図がわからないだろう。
なぜこんなにも任意のタイミングのみで実行できないのだろうと。

だが、ここでコンポーネントとエフェクトのライフサイクルを思い出して欲しい。

コンポーネントのライフサイクルとしては「マウント(初回レンダー)→再レンダー→アンマウント」となるが、エフェクトのライフサイクルは「レンダリングの直後にセットアップを実行して、次にレンダリングが発生したときにはエフェクトをクリーンアップしてから改めてセットアップを実行するを繰り返し、コンポーネントがアンマウントされたときにはエフェクトをクリーンアップする」という流れである。

useEffect(() => {
  // セットアップ
  // ~
  
  // クリーンアップ
  return () => {
    // ~
  };
});

依存配列を使って実行タイミングをコントロールすると考えられがちだが、エフェクトはセットアップとクリーンアップの単純なサイクルの繰り返しであることがわかる。

それを踏まえると、セットアップに対してクリーンアップをきちんとかいてエフェクトをべき等にすることが肝要であることがわかる。

それができていればStrict Modeでマウント直後のエフェクトが二回実行されたとしてもクリーンアップをしっかりと書いていればなにも問題はないのである。

つまり、任意のタイミングで処理を実行するためにuseEffectを使うのではなく、エフェクトの関数がリアクティブな値を一切使っていないときは、依存配列が自然に空になり、関数の中でリアクティブな値を使っているときは機械的に列挙するような使い方が正しく、実行するタイミングに捉われた設計を行うのはナンセンスであるといえる。

useEffectはエスケープハッチ、つまり他の機能で実装できないときに使う最後の手段であるため、なにがなんでもuseEffectで実装するということもナンセンスであるが、比較的頻繁に使うuseEffectに対する勘違いは異常なほど世間で蔓延っていると感じる。自戒を込めて。