useEffect の中で状態を更新する構造は、正しく設計されていれば問題ありません。しかし、依存配列や更新ロジックの設計が曖昧だと、Reactの再レンダリングに伴って useEffect → setState → 再レンダリング → useEffect... のような状態更新の連鎖構造が発生し、無限ループやパフォーマンス劣化を引き起こします。

本記事では、レビューアーとして useEffect の中で行われている状態更新が意図されたものか、構造的に安全かを判断する方法を整理します。

よくある無限ループの構造

パターン1:依存配列と状態更新が一致していない

状態連鎖を引き起こす構造
const MyComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1);
  }, [count]);  // ← countを更新してまた発火
  return <div>{count}</div>;
};
コードレビュー
@Reviewer: 状態の更新により依存配列の値が変化し、useEffectが再実行されるループ構造です。明示的な停止条件がないため、無限再レンダリングが発生します。

このような構造は、状態依存の再計算をuseEffectで処理しようとしているため、Reactの「変更→再評価」サイクルと衝突してしまいます。

構造上の原因を見抜く視点

「状態が再度Effectを呼ぶか?」を構造的に追跡

  • setState() によって変更された値が useEffect の依存配列に含まれていないか
  • setState() によって、自分自身の依存をトリガーしていないか
  • 更新関数が外部の計算結果に依存していないか

避けるべき構造パターンと代替案

パターン2:propsをstateに写す構造

props → stateコピー構造
const MyComponent = ({ value }: { value: number }) => {
  const [internal, setInternal] = useState(value);

  useEffect(() => {
    setInternal(value);
  }, [value]);

  return <div>{internal}</div>;
};
コードレビュー
@Reviewer: propsをstateにコピーする構造は原則避けるべきです。直接propsを使う、またはuseMemoでの変換処理を検討してください。

この構造は一見問題なさそうに見えますが、props → state → propsの再反映という構造をとると、思わぬ再実行や副作用のトリガーになりかねません。

設計改善案:変換をuseMemoに任せる

useMemoによる変換構造
const MyComponent = ({ value }: { value: number }) => {
  const formatted = useMemo(() => value * 100, [value]);
  return <div>{formatted}</div>;
};
コードレビュー
@Reviewer: propsの値を状態に変換せず、memo化された変数として扱うことで、再レンダリングと更新の関係が明示的になっています。

このように、変換・整形と状態保持を明確に分離することで、構造的な再実行の危険を避けられます。

状態更新と依存関係の可視化

UML Diagram

パターン3:非同期処理からの状態更新がトリガーに

非同期処理でのループ誘発
useEffect(() => {
  fetch("/api/count")
    .then(res => res.json())
    .then(data => setCount(data.count));
}, [count]); // ← 状態が変わるたびにfetch発火
コードレビュー
@Reviewer: 状態`count`が変わるたびにAPIが再実行される構造です。初回のみ取得したい場合は依存配列を`[]`にするか、明示的な条件を入れてください。

このような構造は、初回取得なのか、依存に応じた再取得なのかの意図が不明確であり、レビューでは意図と実装の乖離がないかを確認する必要があります。

非同期処理の状態更新は依存外で扱うべき

fetchの構造修正
useEffect(() => {
  fetch("/api/count")
    .then(res => res.json())
    .then(data => setCount(data.count));
}, []); // 初回のみ実行

これにより、明確に「初期ロードのみ」という構造が表現され、レビュー時にも意図が読み取りやすくなります。

構造的判断のチェックポイント(レビュー観点)

useEffectsetStateは構文的に書けてしまうだけに、レビューアーは構造の循環性と依存トリガーに敏感である必要があります。見逃せば即バグ、見抜けば構造改善への第一歩となる重要なレビュー項目です。