React無限ループの原因と解決方法|初心者向けトラブルシューティングガイド

React / Next.js

React無限ループの原因と解決方法|初心者向けトラブルシューティングガイド

Reactで開発をしていると、突然アプリケーションが重くなったり、ブラウザが応答しなくなったりすることがあります。その多くの原因が「無限ループ」です。本記事では、Reactの無限ループが発生する原因から解決方法までを、初心者向けに詳しく解説します。

  1. React無限ループとは?
  2. React無限ループの主な原因
    1. 1. useEffectの依存配列が空または記載されていない
    2. 2. useEffect内で状態を更新している
    3. 3. イベントハンドラ内での状態更新とレンダリング
    4. 4. 参照型の値を依存配列に記載
    5. 5. setState時に直前の状態を使っている
  3. 無限ループの解決手順
    1. ステップ1: 問題のコンポーネントを特定する
    2. ステップ2: useEffectの依存配列を確認
    3. ステップ3: コンポーネント本体の処理を確認
    4. ステップ4: 参照型の値をメモ化する
  4. 具体的なコード例
    1. 【悪い例1】依存配列がない場合
    2. 【良い例1】依存配列を正しく設定
    3. 【悪い例2】オブジェクトが依存配列に含まれている
    4. 【良い例2】useMemoで参照型の値をメモ化
    5. 【悪い例3】レンダリング時にsetStateを呼び出す
    6. 【良い例3】useEffect内で非同期処理を実行
    7. 【悪い例4】イベントハンドラをレンダリング時に定義
    8. 【良い例4】useCallbackでハンドラをメモ化
    9. 【実践例】APIからデータを取得するコンポーネント
  5. Reactの無限ループ問題で初心者がよくする間違い
    1. 間違い1: 依存配列を省略している
    2. 間違い2: 状態を直接更新する代わりに前の状態を参照しない
    3. 間違い3: 子コンポーネントに新しいオブジェクトを毎回渡す
    4. 間違い4: 非同期処理を直接useEffect内で定義しない
    5. 間違い5: useEffectの代わりにuseMemoやuseCallbackを使う
  6. デバッグのコツ
    1. 1. console.logで実行回数を確認
    2. 2. React DevTools を活用
    3. 3. 依存配列を視覚化する
  7. まとめ

React無限ループとは?

React無限ループとは、コンポーネントが何度も何度も再レンダリングを繰り返す現象のことです。正常な場合、Reactは必要なタイミングでコンポーネントを再レンダリングしますが、無限ループが発生すると、その判断が狂い、延々と再レンダリングが続いてしまいます。その結果、CPUの使用率が上昇し、ブラウザが重くなったり、最悪の場合クラッシュしてしまうのです。

React無限ループの主な原因

1. useEffectの依存配列が空または記載されていない

最も多い原因が、useEffectの依存配列の設定ミスです。useEffectは、依存配列に記載された値が変更されたときに実行されます。もし依存配列を記載しなかった場合、毎回のレンダリングで実行され、その中で状態を更新すると無限ループになります。

2. useEffect内で状態を更新している

useEffect内で状態(state)を更新すると、その状態の変更によってコンポーネントが再レンダリングされます。もし依存配列にその状態が含まれていれば、再度useEffectが実行され、また状態が更新される…という無限ループが生まれます。

3. イベントハンドラ内での状態更新とレンダリング

コンポーネント本体(レンダリング時)にイベントハンドラの処理を記載すると、毎回実行され無限ループになります。例えば、onClickハンドラを直接記載すると、レンダリング時に実行されてしまいます。

4. 参照型の値を依存配列に記載

JavaScriptでは、オブジェクトや配列は参照型です。レンダリングのたびに新しいオブジェクト・配列が作成されると、useEffect側では「値が変わった」と判断し、また実行されてしまいます。

5. setState時に直前の状態を使っている

状態を更新する際に、その状態自体を参照しながら更新すると、依存配列にその状態を記載した場合、無限ループになることがあります。

無限ループの解決手順

ステップ1: 問題のコンポーネントを特定する

まず、ブラウザの開発者ツールを開いてコンソールを確認します。Reactが警告メッセージを表示していることがあります。また、無限ループが起きているコンポーネント名を特定することが重要です。

ステップ2: useEffectの依存配列を確認

問題のコンポーネントにuseEffectが使われていれば、依存配列を確認します。以下のパターンをチェックしましょう:

  • 依存配列が記載されていないか?
  • 依存配列が空配列([])になっているか?
  • 状態更新後にその状態が依存配列に含まれているか?

ステップ3: コンポーネント本体の処理を確認

レンダリング時に直接関数呼び出しやsetStateを実行していないか確認します。これらはuseEffect内またはイベントハンドラ内に移動させる必要があります。

ステップ4: 参照型の値をメモ化する

useCallbackやuseMemoを使って、参照型の値をメモ化し、不要な再計算を防ぎます。

具体的なコード例

【悪い例1】依存配列がない場合

function BadComponent() {
  const [count, setCount] = useState(0);

  // 依存配列がないため、毎回のレンダリングで実行される
  useEffect(() => {
    console.log('この行が何度も実行されます');
    setCount(count + 1); // さらに状態を更新するため無限ループ!
  });

  return <div>Count: {count}</div>;
}

【良い例1】依存配列を正しく設定

function GoodComponent() {
  const [count, setCount] = useState(0);

  // 依存配列に空配列を指定し、マウント時のみ実行
  useEffect(() => {
    console.log('マウント時のみ1回実行されます');
    // APIからデータを取得する処理などはここに記載
  }, []);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

【悪い例2】オブジェクトが依存配列に含まれている

function BadFetchComponent() {
  const [data, setData] = useState(null);

  // 毎回新しいオブジェクトが作成され、useEffectが何度も実行される
  const config = { url: 'https://api.example.com/data' };

  useEffect(() => {
    fetch(config.url)
      .then(res => res.json())
      .then(data => setData(data));
  }, [config]); // 無限ループ!

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

【良い例2】useMemoで参照型の値をメモ化

function GoodFetchComponent() {
  const [data, setData] = useState(null);

  // useMemoで値をメモ化し、urlが変更されない限り同じオブジェクトを返す
  const config = useMemo(() => {
    return { url: 'https://api.example.com/data' };
  }, []); // urlは変わらないため、空配列でOK

  useEffect(() => {
    fetch(config.url)
      .then(res => res.json())
      .then(data => setData(data));
  }, [config.url]); // configの中身ではなく、urlのみに依存

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

【悪い例3】レンダリング時にsetStateを呼び出す

function BadStateComponent() {
  const [items, setItems] = useState([]);

  // コンポーネント本体で直接関数呼び出し - これは無限ループの原因!
  const fetchItems = () => {
    fetch('https://api.example.com/items')
      .then(res => res.json())
      .then(data => setItems(data)); // レンダリング時に呼ばれるため無限ループ
  };

  fetchItems(); // 毎回のレンダリングで実行される

  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

【良い例3】useEffect内で非同期処理を実行

function GoodStateComponent() {
  const [items, setItems] = useState([]);

  // useEffect内で非同期処理を実行
  useEffect(() => {
    const fetchItems = async () => {
      const response = await fetch('https://api.example.com/items');
      const data = await response.json();
      setItems(data);
    };

    fetchItems();
  }, []); // マウント時のみ実行

  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

【悪い例4】イベントハンドラをレンダリング時に定義

function BadEventComponent() {
  const [count, setCount] = useState(0);

  // レンダリング時に毎回新しい関数が作成される
  // これ自体は無限ループではないが、子コンポーネントに渡すと問題になる
  const handleClick = () => {
    setCount(count + 1);
  };

  // さらに悪い例:直接setStateを呼び出す
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* 子コンポーネント */}
      <ChildComponent onUpdate={handleClick} />
    </div>
  );
}

【良い例4】useCallbackでハンドラをメモ化

function GoodEventComponent() {
  const [count, setCount] = useState(0);

  // useCallbackで関数をメモ化し、依存配列の値が変わるまで同じ関数を返す
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1); // 前の状態を使用
  }, []); // 依存配列が空なため、マウント後は同じ関数が返される

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <ChildComponent onUpdate={handleClick} />
    </div>
  );
}

function ChildComponent({ onUpdate }) {
  return <button onClick={onUpdate}>Update Parent</button>;
}

【実践例】APIからデータを取得するコンポーネント

import React, { useState, useEffect, useCallback } from 'react';

function UserListComponent() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);

  // ページ番号が変わるたびにデータを再取得
  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(
          `https://api.example.com/users?page=${page}`
        );
        
        if (!response.ok) {
          throw new Error('Failed to fetch users');
        }
        
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, [page]); // pageが変更された時のみ実行

  const handleNextPage = useCallback(() => {
    setPage(prevPage => prevPage + 1);
  }, []);

  const handlePrevPage = useCallback(() => {
    setPage(prevPage => Math.max(prevPage - 1, 1));
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <div>
        <button onClick={handlePrevPage} disabled={page === 1}>
          Previous
        </button>
        <span>Page {page}</span>
        <button onClick={handleNextPage}>Next</button>
      </div>
    </div>
  );
}

export default UserListComponent;

Reactの無限ループ問題で初心者がよくする間違い

間違い1: 依存配列を省略している

多くの初心者が、useEffectの依存配列を記載しないことがあります。「とりあえず動けばいい」という考えで実装すると、無限ループの罠に陥ります。必ず依存配列を記載し、そのコンポーネントがいつ再実行されるべきかを明確にしましょう。

間違い2: 状態を直接更新する代わりに前の状態を参照しない

状態の更新時に、現在の状態の値を直接使用するのではなく、更新関数内で前の状態を参照する習慣をつけましょう。setCount(count + 1)ではなくsetCount(prevCount => prevCount + 1)と記載することで、タイミングの問題を回避できます。

間違い3: 子コンポーネントに新しいオブジェクトを毎回渡す

コンポーネント内で毎回新しいオブジェクトを作成して子コンポーネントに渡すと、子コンポーネント側の依存配列チェックで無限ループが発生する可能性があります。useMemoやuseCallbackを活用しましょう。

間違い4: 非同期処理を直接useEffect内で定義しない

useEffect内で直接asyncキーワードをつけるのではなく、内部で非同期関数を定義して実行するという二段階の書き方を心がけましょう。これにより、クリーンアップ処理も簡単になります。

間違い5: useEffectの代わりにuseMemoやuseCallbackを使う

データ取得など副作用を伴う処理にはuseEffectを、計算結果のメモ化にはuseMemoを、関数のメモ化にはuseCallbackを使うというように、正しいHooksを選択することが大切です。

デバッグのコツ

1. console.logで実行回数を確認

問題のコンポーネントにconsole.logを仕込み、何回実行されているかを確認します。無限ループしていれば、コンソールに無数のメッセージが表示されます。

2. React DevTools を活用

React DevTools (ブラウザ拡張) をインストールすると、コンポーネントのレンダリング回数や、Hooksの状態を詳しく確認できます。これは無限ループの原因特定に非常に有効です。

3. 依存配列を視覚化する

useEffectを使用する際は、コメントで依存配列の意図を明記することをお勧めします。例えば、「pageが変更された時のみ実行」というようにコメントすることで、バグを防げます。

まとめ

Reactの無限ループは、初心者が非常に頻繁に遭遇する問題ですが、原因と対策を理解すれば簡単に解決できます。重要なポイントは以下の通りです:

  • useEffectには必ず依存配列を記載する – 依存配列を省略すると、毎回のレンダリングで実行されてしまいます
  • 状態を更新する際は前の状態を参照するsetState(prevState => ...)の形で記載します
  • 参照型の値はメモ化する – useMemoやuseCallbackを活用します
  • 非同期処理はuseEffect内で実行する – コンポーネント本体では実行しません
  • イベントハンドラはuseCallbackでメモ化する – 特に子コンポーネントに渡す場合

これらのポイントを意識して開発することで、Reactの無限ループ問題は大幅に減らせます。本記事のコード例を参考にしながら、安全で効率的なReactコンポーネントを作成してください。

もし本記事の内容でもまだ問題が解決しない場合は、ブラウザの開発者ツールとReact DevToolsを駆使して、どのコンポーネントがどのタイミングで何回再レンダリングされているかを詳しく調査することをお勧めします。一つひとつ丁寧にチェックしていけば、必ず原因は特定できます。

タイトルとURLをコピーしました