Reactにおける useEffect のベストプラクティスとして「責務が異なる副作用は別々のEffectに分けるべき」という方針が知られています。これは構造的にも正しいアプローチですが、過度に細分化された useEffect はかえって構造の意図を曖昧にし、レビューや保守を困難にする原因になります。

本記事では、useEffect の分割と統合のバランスをレビューアー視点でどう見極めるかを、構造設計の観点から整理します。

分割が有効に働く条件とその目的

まず、useEffect を分ける目的は大きく以下の2つです:

  • 副作用のトリガー条件が異なる(例:Aは初期化、Bは状態変更時)
  • 副作用の対象が異なる(例:一方はDOM操作、他方はAPI通信)

この条件に該当する場合、useEffect を分けることで依存配列を最小限に保ち、構造の意図が明確になるというメリットがあります。

責務ごとに分割された良い例
useEffect(() => {
  // 初回のみ
  initFeature();
}, []);

useEffect(() => {
  // 設定が変更されたときのみ
  updateFeature(flag);
}, [flag]);
コードレビュー
@Reviewer: 初期化と動的更新で分けられており、依存配列も最小で構造が明快です。

過剰分割によって構造が破綻するパターン

パターン1:1つのロジックが複数のEffectにまたがって記述されている

副作用が分散して見通せない構造
useEffect(() => {
  if (step === 1) prepare();
}, [step]);

useEffect(() => {
  if (step === 2) execute();
}, [step]);

useEffect(() => {
  if (step === 3) finalize();
}, [step]);
コードレビュー
@Reviewer: `step` に対する処理が3つのEffectに分かれていることで、構造上の連続性や状態の変遷が読み取りづらくなっています。単一のEffectに統合した方が構造として自然です。

条件分岐の流れや順序が副作用の中に分散してしまうと、状態のトランジションが読みづらくなり、テストや保守も困難になります。

統合が望ましいケースの例

処理が明確に関連しており、同じ依存条件で動く場合

統合した方が構造が明確な例
useEffect(() => {
  if (step === 1) {
    prepare();
  } else if (step === 2) {
    execute();
  } else if (step === 3) {
    finalize();
  }
}, [step]);
コードレビュー
@Reviewer: `step` という状態の変遷を1つのEffectで管理する構造にすることで、遷移の全体像と処理の意図が読みやすくなっています。

可視化:過剰分割による副作用の分散

UML Diagram

パターン2:依存配列が不必要に複雑化する構造

副作用を分割することで、それぞれに対して依存配列を個別に記述する必要が生じ、変更時の追跡コストが上がるリスクがあります。

過剰分割による依存配列の冗長化
useEffect(() => {
  fetchData();
}, [user.id, config.token]);

useEffect(() => {
  validateData();
}, [config.token, user.role]);
コードレビュー
@Reviewer: どちらのEffectも`config.token`を依存に含んでおり、冗長です。副作用の粒度が重複しており、統合によって構造が整理される可能性があります。

分割・統合の判断基準(レビュー観点)

判断項目 統合が望ましいケース
依存対象 同じ変数に依存している
処理目的 処理の流れが連続している
副作用 同一コンテキストで実行されるべき
読みやすさ 分割により読みにくくなっている

構造的に見たとき、「分けたことで本当に見通しが良くなったか?」という観点を持つことが重要です。

カスタムHookに分ける判断との違い

useEffect を分けるのではなく、「責務ごとにHookに切り出す」ことで構造が改善されるケースもあります。

カスタムHookでの責務分離
function useUserInitializer(userId: string) {
  useEffect(() => {
    fetch(`/api/user/${userId}`);
  }, [userId]);
}
利用側
useUserInitializer(user.id);
コードレビュー
@Reviewer: 依存条件が固定されており、責務が明確です。呼び出し元のコンポーネントの構造を簡素に保てています。

このように、useEffectを分けるより、責務単位で抽象化する方が構造上の透明性が高まるケースは多くあります。

レビュー時の確認ポイント

副作用の分割はあくまで責務分離の手段であり、目的ではありません。レビューアーは「分けられた構造が本当に設計として正しいのか」を常に疑い、構造として読みやすく、変更に強い設計になっているかを判断する必要があります。