React useEffect 依存配列とは?初心者向け完全ガイド

React / Next.js

React useEffect 依存配列とは?初心者向け完全ガイド

はじめに

React開発においてuseEffectは頻繁に使用されるHooksの一つです。しかし、依存配列(dependencies array)の正しい使い方を理解していないと、予期しないバグや無限ループといった問題が発生します。この記事では、useEffectの依存配列について、初心者でも理解できるように詳しく解説します。

1. useEffect 依存配列の原因と基本概念

useEffectとは

useEffectは、Reactコンポーネントの副作用(side effect)を扱うためのHooksです。副作用とは、データ取得、購読の設定、DOMの直接操作など、レンダリング中に発生させてはいけない処理を指します。

依存配列とは

依存配列は、useEffectの第2引数として指定する配列です。この配列に含まれた値が変更されたときのみ、useEffectの処理が実行されます。依存配列の指定方法によって、処理の実行タイミングが大きく変わります。

依存配列がない場合の問題

依存配列を指定しない場合、useEffectはレンダリングのたびに実行されます。これにより、以下のような問題が発生します:

  • APIへのリクエストが無限に送信される
  • パフォーマンスが大幅に低下する
  • 予期しないバグが発生する

2. 依存配列の3つのパターン

パターン1:依存配列を指定しない

依存配列を指定しない場合、useEffectはすべてのレンダリング後に実行されます。

useEffect(() => {
  console.log('毎回実行されます');
});

パターン2:空の依存配列を指定する

空の依存配列[]を指定した場合、useEffectはコンポーネントのマウント時(初回レンダリング時)のみ実行されます。アンマウント時にクリーンアップ処理を行う場合は、この方法を使用します。

useEffect(() => {
  console.log('マウント時に1度だけ実行されます');
  
  return () => {
    console.log('アンマウント時にクリーンアップが実行されます');
  };
}, []);

パターン3:依存配列に値を指定する

依存配列に特定の値を指定した場合、その値が変更されたときのみuseEffectが実行されます。これは最も一般的で重要なパターンです。

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

useEffect(() => {
  console.log('countが変更されました:', count);
}, [count]);

3. 実践的な解決手順

ステップ1:依存配列が本当に必要か確認する

まず、useEffectが必要なのか、それとも別のアプローチで解決できるのか確認します。すべての副作用にuseEffectが必要とは限りません。

ステップ2:実行タイミングを決定する

以下の質問に答えることで、必要な依存配列の形式が決まります:

  • このエフェクトはコンポーネントマウント時だけ実行すればいい? → 空配列[]
  • 特定の値が変更されたときだけ実行したい? → その値を配列に含める
  • 毎回実行する必要がある? → 依存配列を省略

ステップ3:必要な依存値をすべて含める

useEffect内で使用しているすべての外部変数を依存配列に含める必要があります。この時点でESLintのプラグイン(eslint-plugin-react-hooks)を使用すると便利です。

4. 詳しいコード例

例1:APIデータ取得(最頻出パターン)

ユーザーIDが変更されたときだけデータを取得する場合:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // userIdが変更されたときだけこの処理を実行
    setLoading(true);
    
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('エラー:', error);
        setLoading(false);
      });
  }, [userId]); // userIdが依存値

  if (loading) return 
読み込み中...
; if (!user) return
ユーザーが見つかりません
; return

{user.name}

{user.email}

; }

例2:イベントリスナーの設定と削除

マウント時にイベントリスナーを設定し、アンマウント時に削除する場合:

import { useEffect } from 'react';

function WindowResizeListener() {
  useEffect(() => {
    // マウント時に実行される
    const handleResize = () => {
      console.log('ウィンドウがリサイズされました:', window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // アンマウント時に実行される(クリーンアップ関数)
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空の依存配列で、マウント/アンマウント時のみ実行

  return 
ウィンドウサイズの変更をリッスン中です
; }

例3:複数の依存値

複数の値に依存する場合:

import { useState, useEffect } from 'react';

function SearchResults({ searchTerm, sortOrder }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    console.log(`検索: "${searchTerm}" (並び順: ${sortOrder})`);
    
    // searchTermまたはsortOrderが変更されたときだけ実行
    const filteredResults = performSearch(searchTerm, sortOrder);
    setResults(filteredResults);
  }, [searchTerm, sortOrder]); // 両方の値を含める

  return (
    
{results.map(result => (
{result.title}
))}
); } function performSearch(term, order) { // 検索ロジック return []; }

例4:タイマーの設定

タイマーを設定し、コンポーネント削除時にクリアする場合:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // マウント時にタイマーを開始
    const timer = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    // アンマウント時にタイマーをクリア
    return () => clearInterval(timer);
  }, []); // マウント時だけ実行

  return 
カウント: {count}
; }

5. よくある間違いと対策

間違い1:依存配列を忘れる

❌ 間違った例:

useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => setData(data));
  // 依存配列なし → 毎回実行される → 無限ループ
});

✅ 正しい例:

useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => setData(data));
}, []); // 空配列で、マウント時のみ実行

間違い2:オブジェクトを依存値として使用する

❌ 間違った例:

const config = { apiUrl: 'https://api.example.com' };

useEffect(() => {
  // configは毎回新しいオブジェクトとして作成される
  // そのため、毎回このエフェクトが実行される
  fetch(config.apiUrl);
}, [config]); // ❌ オブジェクトを依存値にしない

✅ 正しい例:

const config = { apiUrl: 'https://api.example.com' };

useEffect(() => {
  fetch(config.apiUrl);
}, [config.apiUrl]); // ✅ 必要な値だけを依存値に

// または、useMemoを使用
const memoizedConfig = useMemo(() => ({
  apiUrl: 'https://api.example.com'
}), []);

useEffect(() => {
  fetch(memoizedConfig.apiUrl);
}, [memoizedConfig]);

間違い3:必要な依存値を含めない

❌ 間違った例:

const [count, setCount] = useState(0);
const [message, setMessage] = useState('');

useEffect(() => {
  // messageを使用しているのに、依存配列に含めない
  console.log('Count changed:', count, message);
}, [count]); // ❌ messageを含めていない

✅ 正しい例:

const [count, setCount] = useState(0);
const [message, setMessage] = useState('');

useEffect(() => {
  console.log('Count or message changed:', count, message);
}, [count, message]); // ✅ 両方の値を含める

間違い4:関数を依存値として使用する

❌ 間違った例:

function MyComponent() {
  const handleClick = () => {
    console.log('clicked');
  };

  useEffect(() => {
    button.addEventListener('click', handleClick);
    return () => button.removeEventListener('click', handleClick);
  }, [handleClick]); // ❌ 関数は毎回新しく作成されるため、無限ループに
}

✅ 正しい例:

import { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // useCallbackでメモ化

  useEffect(() => {
    button.addEventListener('click', handleClick);
    return () => button.removeEventListener('click', handleClick);
  }, [handleClick]); // ✅ 安全に使用できる
}

間違い5:依存配列に不要な値を含める

❌ 間白った例:

function ParentComponent() {
  const [data, setData] = useState([]);
  const [index, setIndex] = useState(0);

  return (
     setData([...data])}
      index={index}
    />
  );
}

function ChildComponent({ data, onUpdate, index }) {
  useEffect(() => {
    // dataが変更された時だけ実行したいのに...
    console.log('Data changed');
    onUpdate();
  }, [data, onUpdate, index]); // ❌ indexは不要
}

✅ 正しい例:

function ChildComponent({ data, onUpdate }) {
  useEffect(() => {
    console.log('Data changed');
    onUpdate();
  }, [data, onUpdate]); // ✅ 必要な値だけ
}

6. ESLintプラグインの活用

eslint-plugin-react-hooksを使用することで、依存配列の問題を自動的に検出できます。

インストール方法:

npm install eslint-plugin-react-hooks --save-dev

.eslintrc.jsonの設定:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

7. パフォーマンス最適化のコツ

useCallbackの活用

関数を依存値として使用する場合、useCallbackでメモ化することで、不要な再実行を防げます。

useMemoの活用

複雑な計算結果をメモ化することで、パフォーマンスを改善できます。

複数のuseEffectに分割

異なるタイミングで実行したい処理は、複数のuseEffectに分割することが推奨されます。

// ✅ 推奨:異なる目的で複数のuseEffectを使用
useEffect(() => {
  // API呼び出し
}, [id]);

useEffect(() => {
  // ローカルストレージ保存
}, [data]);

useEffect(() => {
  // イベントリスナー設定
}, []);

まとめ

React useEffectの依存配列は、副作用を効率的に管理するための重要な機能です。以下のポイントを押さえることで、バグのない堅牢なコンポーネントを作成できます:

  • 依存配列の3つのパターンを理解する:何も指定しない、空配列、値を指定する
  • 実行タイミングを明確にする:いつこのエフェクトを実行すべきかを考える
  • 必要な依存値をすべて含める:不足していると古い値を参照する
  • 不要な値は含めない:パフォーマンス低下の原因になる
  • ESLintを活用する:自動的に問題を検出できる
  • クリーンアップ関数を活用する:リソースリークを防ぐ

これらのベストプラクティスに従うことで、React開発の質が大幅に向上し、予期しないバグを防ぐことができます。初心者のうちからこれらを意識することで、スキルアップにもつながります。

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