Reactの関数が2回実行される|StrictModeの挙動を理解

ReactのStrictModeで関数が2回実行される理由と対処法を詳しく解説。開発環境での意図的な重複実行の目的と適切な対応方法を初心者向けに説明します。

Learning Next 運営
31 分で読めます

みなさん、Reactで開発していて「なんか関数が2回実行されてる?」と困った経験はありませんか?

「useEffectが2回呼ばれている」 「APIリクエストが重複して送信される」 「console.logが2回表示される」

こんな現象に遭遇して、「バグかな?」と思った方も多いでしょう。

実は、これらの現象の多くはReact StrictModeが原因なんです。 この記事では、StrictModeがなぜ関数を2回実行するのか、その目的と適切な対処法について分かりやすく解説しますよ。

StrictModeって何?基本を理解しよう

まず、「そもそもStrictModeって何?」という疑問から解決していきましょう。

StrictModeの基本的な仕組み

StrictModeは、開発時に潜在的な問題を発見するためのReactの機能です。

簡単に言うと、コードの品質をチェックしてくれる「検査機能」のようなものですね。

// StrictModeの基本的な使用方法
import React from 'react';
import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

このコードを見てみましょう。

<React.StrictMode>でアプリ全体を囲んでいます。 Create React Appでは、デフォルトでStrictModeが有効になっているんです。

つまり、新しいReactプロジェクトを作ると、最初からこの機能が動いているということですね。

StrictModeがチェックする内容

StrictModeは、どんな問題をチェックしてくれるのでしょうか?

// StrictModeが検出する問題の例
const ProblematicComponent = () => {
  const [count, setCount] = useState(0);
  
  // 副作用を持つ処理(問題のある例)
  useEffect(() => {
    console.log('useEffectが実行されました'); // 2回表示される
    
    // 外部APIへのリクエスト
    fetch('/api/data')
      .then(response => response.json())
      .then(data => console.log(data)); // 2回実行される
    
    // DOMの直接操作
    document.title = `カウント: ${count}`; // 2回実行される
  }, [count]);
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        増加
      </button>
    </div>
  );
};

このコードの問題点を順番に説明しますね。

まず、console.logの部分です。

console.log('useEffectが実行されました'); // 2回表示される

StrictModeが有効だと、このログが2回表示されます。 「あれ?なんで2回?」と思うかもしれませんが、これは意図的な動作なんです。

次に、APIリクエストの部分を見てみましょう。

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data)); // 2回実行される

この処理も2回実行されるため、同じAPIに2回リクエストを送ってしまいます。 これは明らかに問題ですよね。

DOMの直接操作も同様です。

document.title = `カウント: ${count}`; // 2回実行される

ページのタイトルが2回設定されることになります。

これらの問題を早期に発見するのが、StrictModeの役割なんです。

なぜ2回実行されるの?その理由を知ろう

「でも、なんで2回も実行するの?」と疑問に思いますよね。

意図的な重複実行の目的

StrictModeは開発環境でのみ以下の処理を2回実行します。

主な対象は以下の通りです。

  • コンポーネントの初期化関数
  • State更新関数
  • useEffect内の処理
  • useMemo、useCallbackの計算
// StrictModeで2回実行される例
const ExampleComponent = () => {
  console.log('コンポーネントがレンダリングされました'); // 2回表示
  
  const [data, setData] = useState(() => {
    console.log('useState初期化関数'); // 2回表示
    return { value: 0 };
  });
  
  useEffect(() => {
    console.log('useEffect実行'); // 2回表示
    
    return () => {
      console.log('useEffectクリーンアップ'); // 2回表示
    };
  }, []);
  
  const memoizedValue = useMemo(() => {
    console.log('useMemo計算'); // 2回表示
    return data.value * 2;
  }, [data.value]);
  
  return <div>値: {memoizedValue}</div>;
};

このコードの実行順序を詳しく見てみましょう。

まず、コンポーネントのレンダリング部分です。

console.log('コンポーネントがレンダリングされました'); // 2回表示

StrictModeでは、コンポーネント本体も2回実行されます。

次に、useStateの初期化関数です。

const [data, setData] = useState(() => {
  console.log('useState初期化関数'); // 2回表示
  return { value: 0 };
});

useStateに関数を渡すと、その関数も2回実行されます。

useEffectも同様です。

useEffect(() => {
  console.log('useEffect実行'); // 2回表示
  
  return () => {
    console.log('useEffectクリーンアップ'); // 2回表示
  };
}, []);

useEffectの中身だけでなく、クリーンアップ関数も2回実行されます。

純粋性のテスト

StrictModeの目的は、関数の純粋性をテストすることです。

純粋でない関数の例を見てみましょう。

// 純粋でない関数の例(問題)
let globalCounter = 0;

const ImpureComponent = () => {
  const [localCount, setLocalCount] = useState(0);
  
  // 副作用のある計算(グローバル変数を変更)
  globalCounter++; // StrictModeで2回実行されると問題になる
  
  useEffect(() => {
    // 外部の状態を変更(副作用)
    window.myGlobalValue = localCount; // 問題のある処理
  }, [localCount]);
  
  return <div>カウント: {localCount}</div>;
};

この例の問題点を説明しますね。

まず、グローバル変数を変更している部分です。

globalCounter++; // StrictModeで2回実行されると問題になる

この処理が2回実行されると、globalCounterが期待値の2倍になってしまいます。

次に、windowオブジェクトを変更している部分です。

window.myGlobalValue = localCount; // 問題のある処理

この処理も2回実行されると、予期しない副作用が発生する可能性があります。

では、純粋な関数の例も見てみましょう。

// 純粋な関数の例(推奨)
const PureComponent = () => {
  const [localCount, setLocalCount] = useState(0);
  
  // 純粋な計算(副作用なし)
  const doubledCount = localCount * 2; // StrictModeでも問題なし
  
  useEffect(() => {
    // 適切にクリーンアップされる処理
    const timer = setInterval(() => {
      console.log('タイマー実行');
    }, 1000);
    
    return () => {
      clearInterval(timer); // クリーンアップで副作用を解消
    };
  }, []);
  
  return <div>カウント: {doubledCount}</div>;
};

この例では、計算処理に副作用がありません。

const doubledCount = localCount * 2; // StrictModeでも問題なし

この計算は何回実行されても同じ結果になるので、安全です。

useEffectでも、適切にクリーンアップしています。

return () => {
  clearInterval(timer); // クリーンアップで副作用を解消
};

タイマーを設定したら、必ずクリーンアップで解除しています。 これなら2回実行されても問題ありませんね。

具体的な問題例とその解決方法

それでは、実際によくある問題とその解決方法を見ていきましょう。

問題例1:APIリクエストが重複してしまう

最もよくある問題が、APIリクエストの重複です。

// 問題のあるコード(APIが2回呼ばれる)
const BadApiExample = () => {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    // StrictModeで2回実行される
    fetch('/api/users')
      .then(response => response.json())
      .then(users => setData(users));
  }, []);
  
  return <div>{data ? `ユーザー数: ${data.length}` : '読み込み中'}</div>;
};

この例では、fetchが2回実行されてしまいます。

同じAPIに2回リクエストを送るのは、サーバーにとっても無駄ですし、データの取得も遅くなってしまいます。

解決方法1:AbortControllerを使用

const GoodApiExample1 = () => {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const abortController = new AbortController();
    
    const fetchData = async () => {
      try {
        const response = await fetch('/api/users', {
          signal: abortController.signal
        });
        const users = await response.json();
        setData(users);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('API エラー:', error);
        }
      }
    };
    
    fetchData();
    
    // クリーンアップでリクエストを中止
    return () => {
      abortController.abort();
    };
  }, []);
  
  return <div>{data ? `ユーザー数: ${data.length}` : '読み込み中'}</div>;
};

この解決方法を詳しく見てみましょう。

まず、AbortControllerを作成します。

const abortController = new AbortController();

AbortControllerは、リクエストをキャンセルするための仕組みです。

次に、fetchにsignalを渡します。

const response = await fetch('/api/users', {
  signal: abortController.signal
});

これで、リクエストをキャンセルできるようになります。

そして、クリーンアップでリクエストを中止します。

return () => {
  abortController.abort();
};

このクリーンアップ関数により、2回目の実行前に1回目のリクエストがキャンセルされます。

解決方法2:フラグを使用した制御

const GoodApiExample2 = () => {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchData = async () => {
      try {
        const response = await fetch('/api/users');
        const users = await response.json();
        
        // マウント状態を確認してから更新
        if (isMounted) {
          setData(users);
        }
      } catch (error) {
        if (isMounted) {
          console.error('API エラー:', error);
        }
      }
    };
    
    fetchData();
    
    return () => {
      isMounted = false;
    };
  }, []);
  
  return <div>{data ? `ユーザー数: ${data.length}` : '読み込み中'}</div>;
};

この方法では、isMountedフラグを使ってコンポーネントの状態を管理しています。

let isMounted = true;

最初はtrueに設定しておきます。

レスポンスを受け取ったときに、フラグをチェックします。

if (isMounted) {
  setData(users);
}

コンポーネントがまだマウントされている場合のみ、状態を更新します。

クリーンアップでフラグをfalseにします。

return () => {
  isMounted = false;
};

これで、アンマウント後に状態更新が行われることを防げます。

問題例2:外部ライブラリの初期化が重複

地図ライブラリなどを使う場合も注意が必要です。

// 問題のあるコード(ライブラリが2回初期化される)
const BadLibraryExample = () => {
  const mapRef = useRef(null);
  
  useEffect(() => {
    // GoogleMapsが2回初期化される
    const map = new google.maps.Map(mapRef.current, {
      center: { lat: 35.6762, lng: 139.6503 },
      zoom: 8
    });
    
    // クリーンアップがないため、要素が重複する
  }, []);
  
  return <div ref={mapRef} style={{ width: '100%', height: '400px' }} />;
};

この例では、GoogleMapsが2回初期化されてしまいます。

地図が重複して表示されたり、メモリリークの原因になったりする可能性があります。

修正版:適切な初期化とクリーンアップ

const GoodLibraryExample = () => {
  const mapRef = useRef(null);
  const mapInstanceRef = useRef(null);
  
  useEffect(() => {
    // 既に初期化されている場合はスキップ
    if (mapInstanceRef.current) {
      return;
    }
    
    // GoogleMapsを初期化
    mapInstanceRef.current = new google.maps.Map(mapRef.current, {
      center: { lat: 35.6762, lng: 139.6503 },
      zoom: 8
    });
    
    return () => {
      // クリーンアップで適切に破棄
      if (mapInstanceRef.current) {
        mapInstanceRef.current = null;
      }
    };
  }, []);
  
  return <div ref={mapRef} style={{ width: '100%', height: '400px' }} />;
};

この修正版では、2つのポイントで改善しています。

まず、重複初期化の防止です。

if (mapInstanceRef.current) {
  return;
}

既にマップが初期化されている場合は、処理をスキップします。

次に、適切なクリーンアップです。

return () => {
  if (mapInstanceRef.current) {
    mapInstanceRef.current = null;
  }
};

アンマウント時にマップインスタンスを削除しています。

問題例3:イベントリスナーの重複登録

ウィンドウのリサイズイベントなどでも問題が起こります。

// 問題のあるコード(イベントリスナーが重複)
const BadEventExample = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };
    
    // イベントリスナーが2回登録される
    window.addEventListener('resize', handleResize);
    
    // クリーンアップがない
  }, []);
  
  return <div>ウィンドウ幅: {windowWidth}px</div>;
};

この例では、同じイベントリスナーが2回登録されてしまいます。

リサイズのたびに関数が2回実行されることになります。

修正版:適切なクリーンアップ

const GoodEventExample = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };
    
    window.addEventListener('resize', handleResize);
    
    // クリーンアップでイベントリスナーを削除
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return <div>ウィンドウ幅: {windowWidth}px</div>;
};

修正版では、クリーンアップでイベントリスナーを削除しています。

return () => {
  window.removeEventListener('resize', handleResize);
};

これで、イベントリスナーの重複登録を防げます。

StrictModeを無効化する方法

「どうしてもStrictModeを止めたい」という場合の方法も紹介しておきますね。

全体での無効化

// index.js でStrictModeを無効化
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

// StrictModeを削除
root.render(<App />);  // StrictModeなし

StrictModeのタグを削除するだけで無効化できます。

部分的な無効化

// 特定のコンポーネントのみStrictMode外
root.render(
  <React.StrictMode>
    <Header />
    <Sidebar />
    {/* MainContentはStrictMode外で動作 */}
  </React.StrictMode>
);

// MainContentを別途レンダリング
const mainRoot = ReactDOM.createRoot(document.getElementById('main'));
mainRoot.render(<MainContent />);

特定の部分だけStrictModeから外すことも可能です。

環境に応じた条件分岐

// 環境に応じた条件分岐
const isDevelopment = process.env.NODE_ENV === 'development';

const AppWrapper = () => {
  if (isDevelopment) {
    return (
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
  }
  
  return <App />;
};

root.render(<AppWrapper />);

この方法では、開発環境でのみStrictModeを有効にできます。

でも、基本的にはStrictModeを有効のままにしておくことをおすすめします

問題を早期発見できるメリットの方が大きいからです。

実践的なデバッグ方法

StrictModeの問題をデバッグする方法も覚えておきましょう。

デバッグ用のログ追加

const DebugComponent = () => {
  const [count, setCount] = useState(0);
  const renderCountRef = useRef(0);
  
  // レンダリング回数をカウント
  renderCountRef.current++;
  
  useEffect(() => {
    console.log(`useEffect実行 - レンダリング回数: ${renderCountRef.current}`);
    
    return () => {
      console.log(`useEffectクリーンアップ - レンダリング回数: ${renderCountRef.current}`);
    };
  }, []);
  
  useEffect(() => {
    console.log(`countが変更されました: ${count}`);
  }, [count]);
  
  console.log(`レンダリング実行 - 回数: ${renderCountRef.current}, count: ${count}`);
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>
        増加
      </button>
    </div>
  );
};

このコンポーネントでは、実行回数を詳しく記録しています。

レンダリング回数をカウントする部分です。

renderCountRef.current++;

useRefを使って、レンダリングのたびにカウントを増やしています。

各useEffectでもログを出力しています。

console.log(`useEffect実行 - レンダリング回数: ${renderCountRef.current}`);

どのタイミングで何回実行されているかを確認できます。

カスタムフックで重複実行を防ぐ

// 重複実行を防ぐカスタムフック
const useEffectOnce = (effect) => {
  const hasRun = useRef(false);
  
  useEffect(() => {
    if (!hasRun.current) {
      hasRun.current = true;
      return effect();
    }
  }, []);
};

// 使用例
const ComponentWithOnceEffect = () => {
  const [data, setData] = useState(null);
  
  useEffectOnce(() => {
    // この処理は確実に1回のみ実行される
    fetch('/api/data')
      .then(response => response.json())
      .then(setData);
  });
  
  return <div>{data ? 'データ読み込み完了' : '読み込み中'}</div>;
};

このカスタムフックは、処理を確実に1回だけ実行します。

フラグでの制御部分を見てみましょう。

const hasRun = useRef(false);

useEffect(() => {
  if (!hasRun.current) {
    hasRun.current = true;
    return effect();
  }
}, []);

hasRunフラグで、まだ実行されていない場合のみ処理を実行します。

本番環境検証用フック

// 本番環境での動作を模倣するフック
const useProductionBehavior = () => {
  const [isProduction, setIsProduction] = useState(false);
  
  useEffect(() => {
    // 本番環境のように1回のみ実行したい場合
    setIsProduction(process.env.NODE_ENV === 'production');
  }, []);
  
  return isProduction;
};

const TestComponent = () => {
  const isProduction = useProductionBehavior();
  const hasInitialized = useRef(false);
  
  useEffect(() => {
    // 本番環境または初回のみ実行
    if (isProduction || !hasInitialized.current) {
      console.log('初期化処理実行');
      hasInitialized.current = true;
    }
  }, [isProduction]);
  
  return <div>テストコンポーネント</div>;
};

この方法で、本番環境と同じような動作を確認できます。

よくある質問と回答

StrictModeについて、よく聞かれる質問をまとめました。

Q1:StrictModeを無効にしても大丈夫?

A:開発時は有効のままにすることを強くおすすめします。

// 適切な対応例
const SafeComponent = () => {
  useEffect(() => {
    const abortController = new AbortController();
    
    // 副作用を適切に管理
    fetchData(abortController.signal);
    
    return () => {
      abortController.abort();
    };
  }, []);
  
  return <div>安全なコンポーネント</div>;
};

StrictModeを無効にするよりも、適切な実装で問題を解決する方が良いです。

本番環境では自動的に無効になるので、心配いりません。

Q2:なぜ本番環境では1回なのに開発環境では2回?

A:開発環境でのみ問題を発見するためです。

const ExplanationComponent = () => {
  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      console.log('開発環境: StrictModeにより2回実行される可能性');
    } else {
      console.log('本番環境: 通常通り1回のみ実行');
    }
  }, []);
  
  return <div>環境による実行回数の違い</div>;
};

開発環境でのみ2回実行することで、問題を早期発見できます。

本番環境では通常通り1回のみ実行されるので、ユーザーには影響ありません。

Q3:StrictModeで見つかる問題は本当に重要?

A:はい、非常に重要です。

StrictModeで見つかる問題は、実際の運用で起こりうる問題を予兆しています。

例えば以下のようなケースです。

  • ユーザーがページを高速で切り替えたとき
  • ネットワークが不安定なとき
  • デバイスのメモリが不足したとき

こうした状況で、副作用のある処理が予期せず動作する可能性があります。

StrictModeはそうした問題を事前に教えてくれる、とても有用な機能なんです。

まとめ:StrictModeと上手に付き合おう

React StrictModeで関数が2回実行される現象について、詳しく解説しました。

重要なポイントをまとめると

  • 開発環境でのみ発生する意図的な動作
  • 潜在的な問題を早期発見するための機能
  • 副作用の適切な管理が重要
  • クリーンアップ関数で副作用を解消
  • 本番環境では通常通り1回のみ実行

対処法のポイント

  • AbortControllerやフラグを使った制御を実装する
  • 適切なクリーンアップ関数を書く
  • 純粋性を保ったコンポーネント設計を心がける
  • 外部ライブラリの適切な初期化・破棄を行う

StrictModeの警告は、コードの品質向上につながる貴重なヒントです。

無効化するのではなく、適切な実装により問題を根本的に解決することが大切ですね。

この機能を理解して活用することで、より堅牢なReactアプリケーションを構築できます。

ぜひ、この記事を参考にして、StrictModeと上手に付き合ってくださいね!

関連記事