1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactのuseEffectクリーンアップ(return)

1
Posted at

useEffect の中で使う return(クリーンアップ関数)は、コンポーネントのアンマウント時や、次のエフェクトが実行される前に「前回の処理を片付ける」ための重要な仕組みである。

1. なぜクリーンアップ(return)が必要か

useEffect 内で外部システムとの接続やタイマーの設定を行った場合、それらを破棄しないとメモリーリーク意図しない重複動作の原因になる。

たとえば、次の処理を行う場合は必ずクリーンアップが必要となる。

  • setInterval / setTimeout の解除
  • イベントリスナーの削除
  • 外部サービス(WebSocketなど)のサブスクリプション解除
  • 非同期処理(APIリクエストなど)のキャンセル

基本的な書き方

useEffect(() => {
  // 1. 副作用のセットアップ
  const id = setInterval(() => {
    console.log("tick");
  }, 1000);

  // 2. クリーンアップ関数を return する
  return () => {
    clearInterval(id);
  };
}, []);

  • return () => {} は関数を返すだけで、実行はReactに委ねられる。
  • return は JSX を返す場所ではなく、後始末用の関数を返す場所である。

2. 依存配列(deps)とクリーンアップの実行タイミング

クリーンアップが「いつ実行されるか」は、依存配列の指定方法によって完全に変わる。

2.1 依存配列なし:毎回のレンダリングで実行

useEffect(() => {
  console.log('effect');
  return () => console.log('cleanup');
}); // 依存配列なし

  • タイミング: 毎回のレンダリング後にエフェクトが走る直前、およびアンマウント時。
  • 注意点: 頻繁にクリーンアップが走るため、負荷の高い処理を入れるとパフォーマンス低下の原因になる。

2.2 空の配列 []:マウント・アンマウント時のみ

useEffect(() => {
  console.log('mounted');
  return () => console.log('unmounted');
}, []);

  • タイミング: コンポーネントが最初に画面に表示された(マウント)時に1回実行され、画面から消えた(アンマウント)時にクリーンアップが1回実行される。

💡 補足:開発環境(Strict Mode)で2回実行される理由
React 18以降、開発環境では [] を指定していても「マウント → アンマウント → マウント」の順でエフェクトが2回実行される。これはバグではなく、「コンポーネントが破棄されたときに、正しくクリーンアップ処理が行われているか」をReactがテストしているためである。本番環境では1回しか実行されないので気にする必要はない。

2.3 依存値あり [dep]:値が変わるたびに実行

useEffect(() => {
  console.log('effect', userId);
  return () => console.log('cleanup', userId);
}, [userId]);

  • タイミング: userId が変更されるたびに、「古い userId でのクリーンアップ」 が実行された後、「新しい userId でのエフェクト」 が実行される。

実行順序の例(userIdが 1 から 2 に変わる場合)

  1. userId = 1 でエフェクト実行
  2. userId2 に変更される
  3. クリーンアップ実行(userId = 1 の状態を参照)
  4. 新しいエフェクト実行(userId = 2 の状態を参照)
  5. アンマウント時にクリーンアップ実行(userId = 2 の状態を参照)

3. 実務でよく使う4つのクリーンアップパターン

3.1 タイマー(setInterval / setTimeout)

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return () => clearInterval(id);
}, []);

クリーンアップを忘れると、コンポーネントが画面から消えてもバックグラウンドでタイマーが動き続け、メモリを消費する。

3.2 イベントリスナー

useEffect(() => {
  const handler = (e) => console.log(e.clientX);
  window.addEventListener('resize', handler);

  return () => window.removeEventListener('resize', handler);
}, []);

addEventListenerremoveEventListener には、全く同じ関数のインスタンスを渡す必要がある。上記のように useEffect 内部で関数を定義していれば、参照がズレることはない。

3.3 サブスクリプション(WebSocket など)

useEffect(() => {
  const channel = chatApi.subscribe('room-1', (msg) => {
    setMessages(prev => [...prev, msg]);
  });

  return () => {
    channel.unsubscribe();
  };
}, []);

3.4 非同期処理(APIフェッチ)のキャンセル・防止

コンポーネントがアンマウントされた後にAPIリクエストが完了し、存在しないStateを更新しようとするエラーを防ぐ。

方法A:AbortController(リクエスト自体をキャンセルする)

useEffect(() => {
  const controller = new AbortController();

  fetch('/api/user', { signal: controller.signal })
    .then(res => res.json())
    .then(data => setData(data))
    .catch(err => {
      if (err.name === 'AbortError') return; // キャンセルによるエラーは無視
    });

  return () => controller.abort();
}, []);

方法B:Booleanフラグ(State更新のみをスキップする)
サードパーティのSDKや、fetch 以外の非同期処理では、フラグを用いた管理がシンプルで扱いやすい。

useEffect(() => {
  let active = true;

  const loadData = async () => {
    const data = await myCustomSdk.get();
    if (active) {
      setData(data);
    }
  };
  loadData();

  return () => {
    active = false; // クリーンアップ時にフラグを折る
  };
}, []);

4. クリーンアップにまつわる注意点とアンチパターン

4.1 useEffectのコールバック自体を async にしてはいけない

useEffect の第一引数に渡す関数は、クリーンアップ関数(または何もなし)を返す必要がある。しかし、async 関数は自動的に Promise を返してしまうため、Reactがクリーンアップ関数を正しく認識できずエラーになる。

// ❌ NGな書き方
useEffect(async () => {
  const data = await fetch('/api');
  return () => { /* 実行されない */ };
}, []);

// ⭕ 正しい書き方
useEffect(() => {
  const init = async () => {
    const data = await fetch('/api');
  };
  init();

  return () => { /* 正しく実行される */ };
}, []);

5. まとめ:そもそも useEffect は必要か?

実務における設計指針として、クリーンアップを書くべきかどうかの基準は以下の通りである。

  • 書くべきケース: タイマー、イベントリスナー、WebSocketなど、Reactの外部システムを制御する場合。
  • 不要なケース: React内部のState更新や、受け取ったpropsに基づく計算のみの場合。

また、近年のReact(React 19以降など)のトレンドとして、データのフェッチに useEffect を直接使うことは少なくなっている。実務では TanStack Query (React Query)SWR などのデータフェッチライブラリ、またはフレームワーク(Next.jsなど)の機能を活用し、極力 useEffect の管理自体を減らす設計が推奨されている。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?