useEffect は副作用を定義するフックとして、ローカルコンポーネントに閉じた処理を扱うのが基本ですが、現場では ReduxReact Context のようなグローバルステートと密結合した構造が多く見られます。

グローバルステートの変更に応じて useEffect を発火させたり、逆に useEffect の中からグローバルステートを更新したりと、双方向に依存する構造ができあがると、設計上の責務が曖昧になり、意図や影響範囲の見通しが悪くなります。

本記事では、レビューアーとしてuseEffect とグローバルステートの相互依存関係を構造的に読み解き、責務分離の提案ができるようになることを目的とします。

グローバルステートに依存したuseEffect構造の例

Redux依存のuseEffect構造
const MyComponent = () => {
  const user = useSelector((state) => state.auth.user);

  useEffect(() => {
    if (user) {
      fetchUserData(user.id).then(setData);
    }
  }, [user]);

  return <div>{data?.name}</div>;
};
コードレビュー
@Reviewer: `user` の変更に依存して非同期処理が発火する構造ですが、グローバルステートが変更されるたびに `useEffect` が再発火し、副作用が外部トリガーに依存しています。構造的な責務が曖昧になっている可能性があります。

このような構造では、「useEffectは本当にこのタイミングで発火すべきか?」という判断が曖昧になります。グローバルステートの変更トリガーが広範囲に及ぶため、影響範囲が予測しづらくなるのです。

よくある相互依存の構造とそのリスク

パターン1:useEffectがグローバルステートを監視し、さらにその変更によってグローバルステートを更新する

相互依存の構造例
const MyComponent = () => {
  const flag = useSelector((state) => state.feature.flag);
  const dispatch = useDispatch();

  useEffect(() => {
    if (flag) {
      dispatch(setFeatureReady(true));
    }
  }, [flag]);
};
コードレビュー
@Reviewer: グローバルステート `flag` の変更を受けて、別のグローバルステートを更新しており、状態の流れがループ構造になります。ローカルで処理するか、Middleware側で制御する構成を検討すべきです。

このような構造は「Fluxアーキテクチャの単方向データフロー」を壊しており、設計破綻の温床になります。

状態の流れを明確化する設計への改善

中間責務をローカルに保持することで構造を緩和する

中間責務での緩和構造
const MyComponent = () => {
  const flag = useSelector((state) => state.feature.flag);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    setReady(flag);
  }, [flag]);

  return ready ? <Panel /> : null;
};
コードレビュー
@Reviewer: グローバル状態の変化をローカルに写し、UI判断の責任をローカルに持たせることで、グローバルストアの再更新を回避しています。

グローバルステートを監視してそのまま書き戻すのではなく、「読み取り」と「書き込み」を分離することで責務が整理されます。

可視化:グローバル状態との相互依存構造

UML Diagram

Contextでも同様の誤用が起こる

Context構造での双方向依存
const ThemeComponent = () => {
  const { mode, setMode } = useTheme();

  useEffect(() => {
    if (mode === "light") {
      setMode("dark"); // ← 状態変更に対して即座に再変更
    }
  }, [mode]);
};
コードレビュー
@Reviewer: `mode` の変更に応じて `setMode` を呼び出す構造は、ユーザー操作や外部トリガーとの競合を引き起こす恐れがあります。構造的に不自然であり、意図を明確にする必要があります。

ReduxやContextとの切り離し方(責務整理のヒント)

  • 読み取りに特化したHookと、更新に特化したHookを分離する
  • 状態変更のトリガーを useEffect ではなく ユーザー操作やルーティングイベントに限定する
  • ローカルで制御可能な状態はローカルに閉じる(useState)

改善パターン:ミドルウェアに処理を移す

副作用的な状態変化は、ミドルウェア(Redux-thunk, Redux-saga, RecoilのEffectなど)に委ねる方が構造上安全です。

Redux-thunkでの設計分離
// thunk関数
const checkFeatureFlag = () => async (dispatch, getState) => {
  const flag = getState().feature.flag;
  if (flag) {
    dispatch(setFeatureReady(true));
  }
};
コンポーネント側
useEffect(() => {
  dispatch(checkFeatureFlag());
}, []);
コードレビュー
@Reviewer: 状態変更に付随する判断処理をMiddlewareに委譲することで、副作用の責務を明確に分離できています。

レビュー観点チェックリスト

グローバルステートは共有された責務を持つ便利な構造ですが、それゆえに useEffect に組み込むと意図しない循環構造を生みやすくなります。レビューアーは「状態の流れ」を静的に読み解き、責務の再配置を提案する立場でコードを観察する必要があります。