Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

ReactのuseEffectで必須!クリーンアップ関数でメモリリークを防ぐ方法を解説

記事のサムネイル

useEffectとは - クリーンアップ関数の重要性

ReactのuseEffectフックは、関数コンポーネントで副作用を扱うための強力なツールです。副作用とは、データフェッチング、サブスクリプション、手動でのDOM操作など、Reactのレンダリングプロセス以外で行われる処理のことを指します。

useEffectは基本的に以下の構文で使用されます:

目次

TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者
useEffect(() => {
  // 副作用を行うコード
  
  // クリーンアップ関数(オプション)
  return () => {
    // コンポーネントのアンマウント時やeffectの再実行前に実行されるコード
  };
}, [依存配列]); // 依存配列に指定した値が変更されたときにeffectが再実行される

ここで特に重要なのが、return文で返されるクリーンアップ関数です。このクリーンアップ関数が、本記事の主題となります。

なぜクリーンアップ関数が重要なのでしょうか?その理由は主に以下の3つです:

  1. メモリリークの防止: 非同期処理やサブスクリプションを設定した場合、コンポーネントがアンマウントされた後もこれらが実行され続けるとメモリリークの原因となります。クリーンアップ関数でこれらを適切に解除することが必要です。

  2. パフォーマンスの最適化: 不要なリソースを解放することで、アプリケーション全体のパフォーマンスを向上させることができます。

  3. 予期せぬバグの防止: コンポーネントがアンマウントされた後に状態を更新しようとするとエラーが発生します。クリーンアップ関数で適切に処理することでこれらのバグを防ぐことができます。

例えば、APIからデータを取得する非同期処理を考えてみましょう:

useEffect(() => {
  let isMounted = true;
  
  const fetchData = async () => {
    const response = await api.getData();
    if (isMounted) {
      setData(response); // コンポーネントがマウントされている場合のみ状態を更新
    }
  };
  
  fetchData();
  
  // クリーンアップ関数
  return () => {
    isMounted = false; // コンポーネントがアンマウントされたことを示すフラグを設定
  };
}, []);

上記の例では、isMounted変数を使ってコンポーネントがマウントされているかどうかを追跡し、クリーンアップ関数でその値をfalseに設定しています。これにより、コンポーネントがアンマウントされた後にsetDataが呼び出されることを防いでいます。

適切なクリーンアップ関数の実装は、Reactアプリケーションの安定性とパフォーマンスを大きく向上させる重要な要素です。この記事の以降のセクションでは、クリーンアップ関数の詳細な使い方と実践的な例を紹介していきます。

Reactの基本的な知識 - 副作用とライフサイクル

Reactアプリケーションにおいて、コンポーネントは特定のライフサイクルを持っています。関数コンポーネントとHooksの導入により、このライフサイクルの考え方は少し変わりましたが、基本的な概念を理解することは重要です。

コンポーネントのライフサイクル

関数コンポーネントのライフサイクルは、大きく分けて以下の3つのフェーズに分けられます:

  1. マウント: コンポーネントが最初にDOMに追加される
  2. 更新: propsやstateの変更によりコンポーネントが再レンダリングされる
  3. アンマウント: コンポーネントがDOMから削除される

useEffectフックは、これらのライフサイクルフェーズに対応するロジックを実装するための手段を提供します。

副作用(Side Effects)とは

Reactにおける「副作用」とは、レンダリング以外で発生する操作のことを指します。具体的には以下のようなものが副作用に含まれます:

  • データフェッチング(APIリクエストなど)
  • DOMの直接操作
  • タイマーの設定(setTimeoutsetInterval
  • イベントリスナーの登録と解除
  • 外部サービスへのサブスクリプション
  • ブラウザのAPIの使用(localStoragenavigatorなど)

これらの操作は、Reactのレンダリングプロセスの外部で行われるため、適切に管理しないとバグやパフォーマンスの問題を引き起こす可能性があります。

useEffectでのライフサイクルの扱い

useEffectを使うことで、関数コンポーネント内でこれらのライフサイクルイベントに対応するロジックを実装できます。

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

function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // マウント時に実行
    console.log('コンポーネントがマウントされました');
    
    const timer = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
    
    // アンマウント時に実行(クリーンアップ関数)
    return () => {
      console.log('コンポーネントがアンマウントされます');
      clearInterval(timer); // タイマーをクリーンアップ
    };
  }, []); // 空の依存配列はマウント時とアンマウント時のみ実行することを示す
  
  return <div>カウント: {count}</div>;
}

上記の例では、useEffect内でsetIntervalを使ってタイマーを設定し、クリーンアップ関数でclearIntervalを呼び出してタイマーを解除しています。これにより、コンポーネントがアンマウントされた後も不要なタイマーが実行され続けることを防いでいます。

依存配列とその役割

useEffectの2番目の引数である依存配列は、効果の再実行のタイミングを制御する重要な要素です:

  1. 空の配列 []: マウント時にのみ実行され、アンマウント時にクリーンアップが実行される
  2. 特定の値を含む配列 [value1, value2]: マウント時と、指定された値が変化した時に実行される
  3. 依存配列なし: レンダリングごとに実行される

依存配列を正しく設定することは、不要な再レンダリングを防ぎ、パフォーマンスを最適化するために重要です。また、クリーンアップ関数は、次回のエフェクト実行前とコンポーネントのアンマウント時に実行されることを理解しておく必要があります。

次のセクションでは、クリーンアップ関数の具体的な役割と仕組みについて詳しく説明します。

クリーンアップ関数とは何か - その役割と仕組み

クリーンアップ関数は、useEffectフックの中で返される関数であり、コンポーネントが再レンダリングされる前やアンマウントされる際に実行されます。これは、前のセクションで説明したReactのライフサイクルと深く関連しています。

クリーンアップ関数の基本的な構造

クリーンアップ関数は、useEffect内で返される関数として定義されます:

useEffect(() => {
  // 副作用のコード
  
  // クリーンアップ関数
  return () => {
    // クリーンアップのコード
  };
}, [依存配列]);

クリーンアップ関数が実行されるタイミング

クリーンアップ関数が実行されるタイミングは2つあります:

  1. コンポーネントがアンマウントされる時:コンポーネントがDOMから削除される直前
  2. 依存配列の値が変わり、次のeffectが実行される前:effect自体が再実行される前に前回のeffectをクリーンアップ

この2つのタイミングを理解することは、適切なクリーンアップロジックを実装する上で非常に重要です。

クリーンアップ関数の主な役割

クリーンアップ関数には、主に以下のような役割があります:

  1. リソースの解放:タイマー、イベントリスナー、サブスクリプションなどを解除
  2. メモリリークの防止:コンポーネントがアンマウントされた後に不要な処理が実行されるのを防ぐ
  3. 状態更新の回避:コンポーネントがアンマウントされた後の状態更新を防ぐ

これらの役割を実現するために、以下のような具体的なアクションをクリーンアップ関数内で行います:

  • clearTimeoutclearIntervalを使用してタイマーをキャンセル
  • イベントリスナーの登録解除(removeEventListenerなど)
  • WebSocketやRxJSなどのサブスクリプションの解除
  • APIリクエストの中断(AbortControllerなど)
  • その他、副作用によって確保されたリソースの解放

クリーンアップ関数の具体例

イベントリスナーのクリーンアップ

useEffect(() => {
  // イベントリスナーを追加
  const handleResize = () => {
    setWindowSize({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };
  
  window.addEventListener('resize', handleResize);
  
  // クリーンアップ関数
  return () => {
    // イベントリスナーを削除
    window.removeEventListener('resize', handleResize);
  };
}, []); // マウント時とアンマウント時のみ実行

非同期処理のクリーンアップ

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;
  
  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data', { signal });
      const data = await response.json();
      setData(data);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Fetch aborted');
      } else {
        setError(error);
      }
    }
  };
  
  fetchData();
  
  // クリーンアップ関数
  return () => {
    // 進行中のfetchリクエストをキャンセル
    controller.abort();
  };
}, []);

クリーンアップ関数がない場合に起こりうる問題

クリーンアップ関数を実装せずに副作用を使用すると、以下のような問題が発生する可能性があります:

  1. メモリリーク:解放されていないリソースがメモリを占有し続ける
  2. 不要な処理の継続:コンポーネントが表示されていないのに処理が続く
  3. エラーの発生:アンマウントされたコンポーネントの状態を更新しようとしてエラーが発生
  4. パフォーマンスの低下:不要なイベントリスナーやタイマーが実行され続ける

これらの問題を防ぐためには、副作用を生じさせるような操作を行う場合は、適切なクリーンアップ関数を用意することが重要です。

次のセクションでは、初心者が陥りがちなuseEffectの誤った使い方について説明します。

初心者が陥りがちなuseEffectの誤った使い方

ReactのuseEffectフックは強力なツールですが、初心者がよく陥る誤りがいくつかあります。特にクリーンアップ関数に関連するミスは、パフォーマンスの問題やバグを引き起こす可能性があります。ここでは、よくある誤りとその解決方法を紹介します。

1. クリーンアップ関数を省略する

最も一般的な誤りは、必要なときにクリーンアップ関数を省略してしまうことです。

// 悪い例
useEffect(() => {
  const subscription = someAPI.subscribe();
  // クリーンアップ関数がない!
}, []);

このコードでは、コンポーネントがアンマウントされてもサブスクリプションが解除されないため、メモリリークが発生します。

// 良い例
useEffect(() => {
  const subscription = someAPI.subscribe();
  
  return () => {
    subscription.unsubscribe();
  };
}, []);

2. 依存配列の設定ミス

依存配列を正しく設定しないと、不要なeffectの再実行やバグを引き起こす可能性があります。

// 悪い例 - 依存配列が空なのに外部の値に依存している
function SearchComponent({ query }) {
  useEffect(() => {
    fetchResults(query);
  }, []); // queryが変わってもeffectは再実行されない
}
// 良い例
function SearchComponent({ query }) {
  useEffect(() => {
    fetchResults(query);
  }, [query]); // queryが変わるたびにeffectが再実行される
}

3. アンマウント後の状態更新

アンマウントされたコンポーネントの状態を更新しようとすると、警告が表示されます。これは、useEffect内で非同期処理を行う際によく発生する問題です。

// 悪い例
useEffect(() => {
  const fetchData = async () => {
    const result = await api.fetchSomething();
    setData(result); // コンポーネントがアンマウントされた後も実行される可能性がある
  };
  
  fetchData();
}, []);
// 良い例
useEffect(() => {
  let isMounted = true;
  
  const fetchData = async () => {
    const result = await api.fetchSomething();
    if (isMounted) {
      setData(result); // コンポーネントがマウントされている場合のみ実行
    }
  };
  
  fetchData();
  
  return () => {
    isMounted = false;
  };
}, []);

4. クリーンアップ関数内での非同期処理

クリーンアップ関数は同期的に実行される必要があり、非同期関数(async/await)を直接使用することはできません。

// 悪い例
useEffect(() => {
  const resource = initializeResource();
  
  return async () => {
    await resource.closeAsync(); // 非同期のクリーンアップ - これは正しく動作しない
  };
}, []);
// 良い例
useEffect(() => {
  const resource = initializeResource();
  
  return () => {
    resource.close(); // 同期的なクリーンアップメソッドを使用する
    // または、非同期クリーンアップを開始するだけで結果を待たない
    resource.closeAsync().catch(console.error);
  };
}, []);

5. 無限ループの発生

依存配列に誤って状態更新関数を含めると、無限ループが発生することがあります。

// 悪い例 - 無限ループの発生
const [data, setData] = useState([]);

useEffect(() => {
  fetchData().then(newData => {
    setData(newData); // このstateの更新がeffectの再実行を引き起こす
  });
}, [data]); // dataが依存配列に含まれている
// 良い例
const [data, setData] = useState([]);

useEffect(() => {
  fetchData().then(newData => {
    setData(newData);
  });
}, []); // 依存配列が空なので、マウント時にのみ実行される

6. コンポーネント内の関数を依存配列に含めない

コンポーネント内で定義された関数をeffect内で使用する場合、その関数を依存配列に含めるか、useCallbackを使用して関数をメモ化する必要があります。

// 悪い例
function SearchComponent({ query }) {
  const fetchResults = () => {
    // queryを使用した処理
  };
  
  useEffect(() => {
    fetchResults();
  }, []); // fetchResultsが依存配列に含まれていない
}
// 良い例1 - 関数を依存配列に含める
function SearchComponent({ query }) {
  const fetchResults = () => {
    // queryを使用した処理
  };
  
  useEffect(() => {
    fetchResults();
  }, [fetchResults]); // fetchResultsを依存配列に含める
}

// 良い例2 - useCallbackを使用
function SearchComponent({ query }) {
  const fetchResults = useCallback(() => {
    // queryを使用した処理
  }, [query]); // queryが変わるたびにfetchResultsが再作成される
  
  useEffect(() => {
    fetchResults();
  }, [fetchResults]); // メモ化された関数を依存配列に含める
}

これらの誤りを理解し、適切に対処することで、useEffectと関連するクリーンアップ関数を効果的に活用できるようになります。次のセクションでは、メモリリークを防ぐためのクリーンアップ関数の具体的な実装方法について説明します。

メモリリークを防ぐためのクリーンアップ関数の実装方法

Reactアプリケーションでは、メモリリークはパフォーマンスの低下や予期せぬバグの原因となります。特にuseEffectを使用する際には、適切なクリーンアップ関数を実装することでメモリリークを防ぐことが重要です。ここでは、一般的なシナリオごとのクリーンアップ関数の実装方法を説明します。

非同期処理のクリーンアップ

非同期処理を行うuseEffectでは、コンポーネントがアンマウントされた後も処理が続いてしまう可能性があります。以下のパターンを使用して、このような状況を防ぎましょう。

マウント状態のフラグを使用する方法

useEffect(() => {
  let isMounted = true; // マウント状態を追跡するフラグ
  
  const fetchData = async () => {
    try {
      const response = await api.fetchData();
      // マウントされている場合のみ状態を更新
      if (isMounted) {
        setData(response);
      }
    } catch (error) {
      // マウントされている場合のみエラー状態を更新
      if (isMounted) {
        setError(error);
      }
    }
  };
  
  fetchData();
  
  // クリーンアップ関数
  return () => {
    isMounted = false;
  };
}, []);

AbortControllerを使用する方法(fetch APIの場合)

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;
  
  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data', { signal });
      const data = await response.json();
      setData(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        setError(error);
      }
    }
  };
  
  fetchData();
  
  // クリーンアップ関数
  return () => {
    controller.abort(); // 進行中のリクエストをキャンセル
  };
}, []);

タイマーのクリーンアップ

setTimeoutsetIntervalを使用する場合は、クリーンアップ関数で必ずタイマーをクリアする必要があります。

useEffect(() => {
  // タイマーのセット
  const timerId = setTimeout(() => {
    setMessage('タイムアウトしました!');
  }, 5000);
  
  // クリーンアップ関数
  return () => {
    clearTimeout(timerId); // タイマーのクリア
  };
}, []);
useEffect(() => {
  // インターバルのセット
  const intervalId = setInterval(() => {
    setCount(prevCount => prevCount + 1);
  }, 1000);
  
  // クリーンアップ関数
  return () => {
    clearInterval(intervalId); // インターバルのクリア
  };
}, []);

イベントリスナーのクリーンアップ

DOMイベントリスナーを追加した場合は、クリーンアップ関数で必ず削除する必要があります。

useEffect(() => {
  // イベントハンドラの定義
  const handleScroll = () => {
    // スクロール処理
    setScrollPosition(window.scrollY);
  };
  
  // イベントリスナーの追加
  window.addEventListener('scroll', handleScroll);
  
  // クリーンアップ関数
  return () => {
    // イベントリスナーの削除
    window.removeEventListener('scroll', handleScroll);
  };
}, []);

WebSocketやその他のサブスクリプションのクリーンアップ

WebSocketやRxJS、ライブラリ固有のサブスクリプションなどを使用する場合は、クリーンアップ関数でこれらを適切に解除する必要があります。

// WebSocketの例
useEffect(() => {
  const socket = new WebSocket('wss://example.com/socket');
  
  socket.onmessage = (event) => {
    const data = JSON.parse(event.data);
    setMessages(prev => [...prev, data]);
  };
  
  socket.onopen = () => {
    setIsConnected(true);
  };
  
  socket.onclose = () => {
    setIsConnected(false);
  };
  
  // クリーンアップ関数
  return () => {
    socket.close(); // WebSocketの接続を閉じる
  };
}, []);
// RxJSの例
useEffect(() => {
  const subscription = someObservable.subscribe(
    value => setData(value),
    error => setError(error)
  );
  
  // クリーンアップ関数
  return () => {
    subscription.unsubscribe(); // サブスクリプションの解除
  };
}, []);

メモリリーク防止のベストプラクティス

  1. 常にクリーンアップ関数を返す: 副作用を生じさせるコードがある場合は、必ずクリーンアップ関数を実装しましょう。

  2. 依存配列を適切に設定: 依存配列を正しく設定することで、不要なuseEffectの再実行を防ぎ、クリーンアップ関数が頻繁に呼び出されるのを避けられます。

  3. リソースの解放を確認: クリーンアップ関数で確実にすべてのリソースが解放されていることを確認しましょう。

  4. 条件付きのコード実行に注意: 条件付きでuseEffectを実行するためには、useEffect内で条件をチェックするようにし、useEffect自体を条件付きで使用しないようにしましょう。

  5. 非同期クリーンアップを避ける: クリーンアップ関数は同期的に実行されるため、非同期関数を直接使用するのではなく、適切な方法で非同期操作を処理する必要があります。

適切なクリーンアップ関数を実装することで、メモリリークを防ぎ、Reactアプリケーションのパフォーマンスと安定性を向上させることができます。次のセクションでは、useEffectとクリーンアップ関数の実践的な使用例について説明します。

useEffectとクリーンアップ関数の実践的な使用例

ここでは、実際のReactアプリケーション開発でよく遭遇するシナリオにおける、useEffectとクリーンアップ関数の具体的な使用例を紹介します。これらの例を参考にすることで、実際のプロジェクトでの適用方法を理解できるでしょう。

1. データ取得と検索機能の実装

検索キーワードが変更されるたびにAPIからデータを取得し、コンポーネントがアンマウントされた場合には進行中のリクエストをキャンセルする例です。

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

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // 空のクエリの場合は処理しない
    if (!query.trim()) {
      setResults([]);
      return;
    }
    
    // 検索処理の開始
    setIsLoading(true);
    setError(null);
    
    // AbortControllerの作成
    const controller = new AbortController();
    const signal = controller.signal;
    
    // 検索APIの呼び出し
    const fetchResults = async () => {
      try {
        const response = await fetch(
          `https://api.example.com/search?q=${encodeURIComponent(query)}`,
          { signal }
        );
        
        if (!response.ok) {
          throw new Error(`API error: ${response.status}`);
        }
        
        const data = await response.json();
        setResults(data);
        setIsLoading(false);
      } catch (error) {
        // リクエストがキャンセルされた場合は無視
        if (error.name === 'AbortError') {
          console.log('Search aborted');
        } else {
          setError(error.message);
          setIsLoading(false);
        }
      }
    };
    
    fetchResults();
    
    // クリーンアップ関数 - コンポーネントがアンマウントされるか
    // queryが変更されて再度effectが実行される前に呼ばれる
    return () => {
      controller.abort(); // 進行中のリクエストをキャンセル
    };
  }, [query]); // queryが変更されるたびにeffectが再実行される
  
  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (results.length === 0) return <div>結果が見つかりませんでした</div>;
  
  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

2. 外部データソースのサブスクリプション

WebSocketを使用してリアルタイムデータを取得し、コンポーネントがアンマウントされた際に接続を閉じる例です。

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

function LiveDataFeed({ endpoint }) {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);
  
  useEffect(() => {
    // WebSocket接続の確立
    const socket = new WebSocket(endpoint);
    
    socket.onopen = () => {
      setIsConnected(true);
      console.log('WebSocket接続が確立されました');
    };
    
    socket.onmessage = (event) => {
      const newMessage = JSON.parse(event.data);
      setMessages(prevMessages => [...prevMessages, newMessage]);
    };
    
    socket.onerror = (error) => {
      console.error('WebSocketエラー:', error);
    };
    
    socket.onclose = () => {
      setIsConnected(false);
      console.log('WebSocket接続が閉じられました');
    };
    
    // クリーンアップ関数
    return () => {
      console.log('WebSocket接続をクリーンアップします');
      socket.close();
    };
  }, [endpoint]); // endpointが変更されたら再接続
  
  return (
    <div>
      <div>接続状態: {isConnected ? '接続中' : '未接続'}</div>
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message.text}</li>
        ))}
      </ul>
    </div>
  );
}

3. ウィンドウサイズの監視

ウィンドウのリサイズイベントを監視し、コンポーネントがアンマウントされた際にイベントリスナーを解除する例です。

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

function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  
  useEffect(() => {
    // リサイズハンドラの定義
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };
    
    // 現在のウィンドウサイズを設定
    handleResize();
    
    // イベントリスナーの登録
    window.addEventListener('resize', handleResize);
    
    // クリーンアップ関数
    return () => {
      // イベントリスナーの解除
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空の依存配列 - マウント時とアンマウント時のみ実行
  
  return (
    <div>
      <p>ウィンドウの幅: {windowSize.width}px</p>
      <p>ウィンドウの高さ: {windowSize.height}px</p>
    </div>
  );
}

4. タイマーベースのアニメーション

requestAnimationFrameを使用したアニメーションを実装し、コンポーネントがアンマウントされた際にキャンセルする例です。

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

function AnimatedCounter({ targetValue, duration = 1000 }) {
  const [displayValue, setDisplayValue] = useState(0);
  const startTimeRef = useRef(null);
  const startValueRef = useRef(0);
  const animationFrameIdRef = useRef(null);
  
  useEffect(() => {
    startValueRef.current = displayValue;
    startTimeRef.current = null;
    
    // アニメーションフレームの処理
    const animateValue = (timestamp) => {
      if (!startTimeRef.current) {
        startTimeRef.current = timestamp;
      }
      
      const elapsed = timestamp - startTimeRef.current;
      const progress = Math.min(elapsed / duration, 1);
      
      // イージング関数(オプション)
      const easeOutQuad = t => t * (2 - t);
      const easedProgress = easeOutQuad(progress);
      
      // 現在の表示値を計算
      const currentValue = Math.round(
        startValueRef.current + (targetValue - startValueRef.current) * easedProgress
      );
      
      setDisplayValue(currentValue);
      
      // アニメーションが完了していない場合は次のフレームをリクエスト
      if (progress < 1) {
        animationFrameIdRef.current = requestAnimationFrame(animateValue);
      }
    };
    
    // アニメーションの開始
    animationFrameIdRef.current = requestAnimationFrame(animateValue);
    
    // クリーンアップ関数
    return () => {
      // アニメーションフレームのキャンセル
      if (animationFrameIdRef.current) {
        cancelAnimationFrame(animationFrameIdRef.current);
      }
    };
  }, [targetValue, duration]);
  
  return <div className="counter">{displayValue}</div>;
}

5. フォーム入力の自動保存

ユーザーの入力を自動的に保存するタイマーを設定し、コンポーネントがアンマウントされた際にクリアする例です。

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

function AutoSavingForm() {
  const [formData, setFormData] = useState({ title: '', content: '' });
  const [isSaving, setIsSaving] = useState(false);
  const [lastSaved, setLastSaved] = useState(null);
  
  // フォームデータが変更されたときの処理
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };
  
  // データの保存処理
  const saveData = async () => {
    setIsSaving(true);
    
    try {
      // APIへの保存処理(実際のコードに置き換えてください)
      await fetch('https://api.example.com/save', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });
      
      setLastSaved(new Date());
    } catch (error) {
      console.error('保存エラー:', error);
    } finally {
      setIsSaving(false);
    }
  };
  
  // 自動保存の効果
  useEffect(() => {
    // フォームが空の場合は自動保存しない
    if (!formData.title && !formData.content) return;
    
    // タイマーのセット - 2秒間入力がないと自動保存
    const timerId = setTimeout(() => {
      saveData();
    }, 2000);
    
    // クリーンアップ関数
    return () => {
      clearTimeout(timerId); // タイマーのクリア
    };
  }, [formData]); // formDataが変更されるたびにeffectが再実行される
  
  return (
    <form>
      <div>
        <label htmlFor="title">タイトル:</label>
        <input
          type="text"
          id="title"
          name="title"
          value={formData.title}
          onChange={handleChange}
        />
      </div>
      <div>
        <label htmlFor="content">内容:</label>
        <textarea
          id="content"
          name="content"
          value={formData.content}
          onChange={handleChange}
        />
      </div>
      <div>
        {isSaving ? '保存中...' : lastSaved && `最終保存: ${lastSaved.toLocaleTimeString()}`}
      </div>
    </form>
  );
}

これらの実践的な例は、実際のアプリケーション開発における一般的なシナリオを示しています。適切なクリーンアップ関数を実装することで、メモリリークを防ぎ、パフォーマンスを向上させることができます。

まとめ - useEffectのクリーンアップ関数を習得しよう

この記事では、ReactのuseEffectフックにおけるクリーンアップ関数の重要性と、その実装方法について詳しく解説しました。以下に、記事の主要なポイントをまとめます。

学んだこと

  1. クリーンアップ関数の重要性

    • メモリリークの防止
    • パフォーマンスの最適化
    • 予期せぬバグの防止
  2. ReactのライフサイクルとuseEffect

    • コンポーネントのマウント、更新、アンマウントの各フェーズでの挙動
    • 副作用(Side Effects)の適切な管理方法
  3. クリーンアップ関数の基本と仕組み

    • クリーンアップ関数が実行されるタイミング
    • クリーンアップ関数の主な役割と一般的な使用パターン
  4. 初心者が陥りがちな誤り

    • クリーンアップ関数の省略
    • 依存配列の設定ミス
    • アンマウント後の状態更新
    • その他の一般的なミス
  5. メモリリーク防止のための実装方法

    • 非同期処理のクリーンアップ
    • タイマーのクリーンアップ
    • イベントリスナーのクリーンアップ
    • WebSocketやサブスクリプションのクリーンアップ

実践のための次のステップ

この記事で学んだ知識を活かすために、以下のステップを実践してみましょう:

  1. 既存のReactプロジェクトを見直し、クリーンアップ関数が適切に実装されているか確認する
  2. メモリリークの可能性がある部分を特定し、クリーンアップ関数を追加する
  3. カスタムフックを作成して、一般的なパターンを抽象化する
  4. パフォーマンスプロファイリングツールを使用して、クリーンアップ関数の効果を測定する

正しいクリーンアップ関数の実装は、安定性とパフォーマンスに優れたReactアプリケーションを構築するための重要な要素です。この記事が、皆さんのReact開発のレベルアップに役立つことを願っています。

おすすめコンテンツ