Reactの useEffect によるAPI通信は一般的なパターンですが、コードレビューを行っていると「毎回呼ばれるのは仕様か設計ミスか?」という判断に迷う場面に多く出会います。これは、依存配列の設計・副作用の責務・状態変化との関係が曖昧なまま構造が組まれているために起きる問題です。

本記事では、useEffect によってAPIが毎回呼ばれてしまう構造をレビューアーとしてどのように見極め、指摘・改善していくかを整理します。

APIが「毎回呼ばれる」構造とは?

まず確認すべきは、以下のようなコードです。

毎回fetchが発火する構造
const MyComponent = ({ userId }: { userId: string }) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(`/api/user/${userId}`)
      .then((res) => res.json())
      .then((json) => setData(json));
  }, []); // ← 一見問題なさそうに見える
};

この構造は一見、初回だけfetchが走るように見えますが、実際にはuserId が変わっても再取得されない構造となっており、逆に必要な再取得が行われないバグになります。

逆に、以下のような構造では依存の変更のたびにfetchが走ることになります。

依存変数が更新されるたびにAPI発火
useEffect(() => {
  fetch(`/api/user/${userId}`)
    .then((res) => res.json())
    .then(setData);
}, [userId]);

この構造は一見正しいですが、userId の変化頻度と粒度に応じて副作用の呼び出しが過剰になる可能性があります。

過剰なAPI呼び出しの設計ミス例

パターン1:依存配列に意味のない変数を含めている

無関係な変数がトリガーに
const [count, setCount] = useState(0);

useEffect(() => {
  fetch("/api/data").then(res => res.json()).then(setData);
}, [count]); // ← UI上のカウント変更でfetchが走る
コードレビュー
@Reviewer: `count` はfetch処理と無関係であり、依存配列に含まれる理由が不明です。副作用の発火条件として妥当か、責務との整合性を再検討してください。

パターン2:fetchを状態更新のたびに呼んでしまう構造

状態更新による再発火構造
useEffect(() => {
  fetch(`/api/user/${userId}`)
    .then(res => res.json())
    .then(json => {
      setUser(json);
      setLastUpdated(new Date()); // 状態更新
    });
}, [userId, lastUpdated]); // ← lastUpdatedでループ
コードレビュー
@Reviewer: `lastUpdated` を更新しているのに、それが依存配列に含まれていることで無限にEffectが再実行されます。依存関係と更新対象を明確に分離してください。

依存関係とfetchの発火条件を整理する視点

原則:APIは「いつ呼ばれるべきか?」を先に設計する

レビュー観点では、以下を必ず確認します:

  • この fetch は 初回だけ で良いのか?
  • 状態の変化に応じて 再取得が必要なのか?
  • 何が変更されたときに副作用が走るべきか?

依存配列の構成はこの判断から逆算する形で決定するべきです。

発火条件を明確にした設計例

userId変更時のみfetchする設計
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/user/${userId}`, { signal: controller.signal })
    .then(res => res.json())
    .then(setUser)
    .catch((err) => {
      if (err.name !== "AbortError") console.error(err);
    });

  return () => controller.abort();
}, [userId]);
コードレビュー
@Reviewer: `userId` の変化時にのみfetchが実行され、かつAbortControllerによって多重実行時の競合が防がれています。責務が明確な構造です。

可視化:過剰発火と意図的な再取得構造の違い

UML Diagram

カスタムHookへの切り出しで構造を明確に

useUserというHookにfetch責務を委譲
function useUser(userId: string) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/user/${userId}`, { signal: controller.signal })
      .then(res => res.json())
      .then(setUser)
      .catch((err) => {
        if (err.name !== "AbortError") console.error(err);
      });

    return () => controller.abort();
  }, [userId]);

  return user;
}
利用側
const user = useUser("abc123");
コードレビュー
@Reviewer: fetch処理をHookに切り出すことで、責務が明確化され、利用側からは「ユーザー情報を得る」だけという直感的な構造になっています。

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

Reactの useEffect によるAPI呼び出しは、機能要件としては簡単に満たせてしまうため、構造の問題が後回しにされやすい領域です。レビューアーは「それは本当に毎回呼ばれるべき副作用か?」という視点から、構造の再設計を提案していくことが求められます。