[React #04] useEffectフック徹底解説:副作用の管理とパフォーマンス最適化

1. はじめに

副作用 (Side Effects) の概念となぜ useEffect が重要か。

副作用 (Side Effects) の概念

Reactにおける「副作用(side effects)」は、コンポーネントがUIのレンダリング以外の処理を行う際に発生するものを指します。

これらの副作用は、コンポーネント内で非同期処理や外部のデータ操作、DOMの直接操作などが必要な場合に関わります。

副作用の例

副作用には以下のようなものが含まれます:

  • APIからデータを取得する処理
     (データフェッチ)

  • タイマーの設定
     ( setTimeout や setInterval )

  • DOMの操作
     (直接的なDOM変更やフォーカス設定)

  • サードパーティライブラリとのインタラクション
     (例えば、jQueryやGoogle Maps APIの利用)

  • ブラウザのローカルストレージやセッションストレージの操作 .

これらの処理は、UIのレンダリングと直接的に関係しないが、コンポーネントの状態に影響を与えるため、Reactのレンダリングフローとは切り離して管理しなければならないのです。

Reactでは、レンダリングを最小限に保ちつつUIの状態を保つために外部とのやり取りを行うことがよくあります。これを「副作用」と呼びます。

副作用を適切に管理する重要性

Reactはコンポーネントが再レンダリングされるたびに、その状態を反映するための処理(レンダリング)を行います。しかし、副作用のある処理(APIの呼び出しやDOMの操作など)をそのままレンダリング処理の中に入れてしまうと、無駄な再レンダリングや予期しない挙動を引き起こす可能性があります。

副作用を管理しない場合の問題

副作用のある処理を適切に管理しないと、次のような問題が発生することがあります:

  • 無限ループ:
     APIを呼び出してデータを取得する処理をコンポーネントの中で無限に実行してしまう。

  • 不必要な再レンダリング:
     API呼び出しやDOM操作が再レンダリングと同時に何度も行われ、パフォーマンスが低下する。

  • コンポーネントの破棄後の処理:
     コンポーネントがアンマウントされた後にも、古い状態で処理が実行されてしまう。

これらの問題を防ぐために、Reactでは副作用の処理をライフサイクルに基づいて管理するための仕組みが必要です。

useEffectフックの登場

Reactのクラスコンポーネントでは、ライフサイクルメソッド(componentDidMountcomponentDidUpdatecomponentWillUnmount)を使用して副作用を管理していました。しかし、関数コンポーネントの登場により、関数コンポーネントでも副作用を適切に管理できる必要が出てきました。

useEffectは、関数コンポーネントでも副作用を簡単に処理できるようにするために登場したReactフックです。

useEffectを使うことで、副作用のある処理をコンポーネントのレンダリングとは別に管理でき、無駄な再レンダリングや予期しない副作用を防ぐことができます。

useEffect の目的

useEffect の目的は、コンポーネントのライフサイクルに合わせた副作用の管理です。これにより、コンポーネントがマウント(表示)、更新、アンマウント(非表示)されるタイミングに応じて、副作用を発生させることができます。

  • マウント時(初回レンダリング後):
     コンポーネントが表示された後に一度だけ処理を実行

  • 更新時(依存関係が変化した場合):
     特定の依存関係が更新されるたびに副作用を実行

  • アンマウント時(コンポーネントの非表示):
     コンポーネントが破棄される前に処理をクリーンアップ

結び

useEffectは、副作用をコンポーネントのライフサイクルに合わせて管理するために非常に重要なフックです。

副作用を適切に管理することで、無駄な再レンダリングや副作用によるバグを防ぎ、Reactアプリケーションを効率的で安定したものにすることができます。

2. useEffect の構文とパラメータ解説

useEffect は、Reactコンポーネントの副作用を管理するためのフックです。副作用を管理するための構文とパラメータの使い方をしっかり理解して、効果的に副作用を処理できるようにしましょう。

基本的な構文

useEffect は以下のように使います:

useEffect(() => {
  // 副作用の処理
}, [依存配列]);
  • 第一引数(副作用の処理):
     副作用を実行するためのコールバック関数を指定します。この関数内で、APIのデータフェッチやタイマーの設定、DOM操作などの処理を行います。
  • 第二引数(依存配列):
     配列の中に指定した値が変化したときのみ、コールバック関数が実行されるように制御します。この配列が**依存関係(dependencies)**を表しており、指定した値が変更されると、再度副作用が実行されます。

例:基本的な useEffect の使い方

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // APIからデータを取得
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(json => setData(json));
  }, []); // 空の配列: コンポーネントのマウント時のみ実行される

  return (
    <div>
      {data ? <p>Data: {data}</p> : <p>Loading...</p>}
    </div>
  );
}

ここでは、useEffect を使ってコンポーネントがマウントされたときに一度だけデータを取得し、data ステートに格納しています。第二引数の空の配列 [] は、依存関係がないことを示し、コンポーネントが最初にマウントされるときのみ処理が実行されます。

依存配列(Dependency Array)の使い方

依存配列([])は、useEffect がいつ実行されるかを制御するために使います。配列内に指定した変数が変更された場合にのみ、副作用の処理が再実行されます。

1. 空の依存配列 []

useEffect に空の依存配列を指定すると、コンポーネントの初回レンダリング時にのみ副作用が実行されます。この方法は、例えばデータフェッチや、初期化処理に使います。

useEffect(() => {
  console.log("コンポーネントがマウントされたときに一度だけ実行される");
}, []); // 空の配列: マウント時のみ実行

2. 特定の依存関係

依存配列に特定の値を指定すると、その値が変更された場合にのみ、副作用が再実行されます。たとえば、APIのデータを表示するコンポーネントで、id が変わったときに新しいデータを取得する場合のように使います。

useEffect(() => {
  console.log("idが変更されたときに実行される");
  fetchData(id);
}, [id]); // idが変更されたときのみ実行

この例では、id が変更されるたびに、データの再取得が行われます。

3. 依存配列なしで副作用を実行

もし依存配列を指定しないと、コンポーネントが更新されるたびに副作用の処理が毎回実行されます。無限に実行されることを避けるため、依存配列は慎重に指定する必要があります。

useEffect(() => {
  console.log("コンポーネントの更新時に毎回実行される");
}); // 依存配列なし: 毎回実行

このコードでは、コンポーネントが再レンダリングされるたびに、useEffect 内の処理が実行されます。


useEffectの実行タイミング

Reactコンポーネントには、レンダリングに関連するライフサイクルが存在し、useEffect はこれらのライフサイクルのタイミングに基づいて実行されます。

  • コンポーネントがマウントされた後(初回レンダリング後) に副作用を実行。
  • コンポーネントが更新されるたびに、副作用を再実行することができます(依存配列を使って管理)。
  • コンポーネントがアンマウントされる前にクリーンアップを行うことも可能です。

これらをコンポーネントのライフサイクルに合わせて管理することが、 useEffect の強力なポイントです。


結び

useEffect は、コンポーネントのライフサイクルに基づいて副作用を管理できるReactフックです。これを使うことで、以下のような利点があります:

  1. 副作用を外部のデータ取得、DOM操作から切り離して管理
  2. 必要なタイミングに副作用を実行でき、無駄な処理を減らすことができる。
  3. コンポーネントのライフサイクルに合わせた副作用の実行とクリーンアップが可能。

3. ライフサイクルとの関係

useEffect を理解するためには、React コンポーネントのライフサイクルとの関係をしっかり把握することが重要です。React では、コンポーネントがどのように**マウント(初回描画)**され、更新され、**アンマウント(破棄)**されるかが、副作用を適切に処理するためのポイントになります。

useEffect はコンポーネントのライフサイクルに合わせて処理を実行するため、これを理解することで、より効率的な副作用の管理ができるようになります。

1. マウント時(初回レンダリング後)

useEffectは、コンポーネントがマウントされた後(初回のレンダリング後)に一度だけ実行される副作用を設定するために使います。これを使うことで、コンポーネントが表示されたタイミングでのみ特定の処理(例えば、APIからのデータ取得)を実行できます。

例: マウント後のデータフェッチ

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // APIからデータをフェッチ
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(json => setData(json));
  }, []); // 空の依存配列:最初のレンダリング後のみ実行される

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

ここでは、useEffectの空の依存配列([]を使って、コンポーネントが初回レンダリング後にデータを一度だけ取得する処理を行っています。これにより、無駄なAPI呼び出しを避けることができます。

2. 更新時(依存関係の変更時)

useEffectの依存配列に指定した変数(依存関係)が変更されるたびに、副作用の処理を再実行することができます。これを使うと、特定の状態が変わったときに、その状態に基づく副作用を発生させることができます。

例: 依存関係が変更されたときにデータを再フェッチ

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    // userIdが変更されたときにデータを再フェッチ
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(json => setProfile(json));
  }, [userId]); // userIdが変更されたときのみ実行される

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

この例では、userId が変更されるたびにuseEffectが再実行され、最新のユーザーデータがフェッチされます。依存関係を使うことで、特定の状態の変更に応じて副作用を処理することができます。

3. アンマウント時(コンポーネントの破棄時)

useEffectでは、クリーンアップ処理を行うためにアンマウント時の処理も定義できます。これは、コンポーネントが破棄される直前に実行される処理です。例えば、タイマーを解除したり、外部ライブラリのリソースを解放したりする処理に使います。

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

import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // クリーンアップ処理:コンポーネントがアンマウントされる時にタイマーをクリア
    return () => clearInterval(intervalId); // アンマウント時に実行
  }, []); // 初回マウント時にタイマーを開始

  return <div>Seconds: {seconds}</div>;
}

この例では、setIntervalを使って秒数をカウントしていますが、コンポーネントがアンマウントされる際にタイマーをクリアするために、return でクリーンアップ処理を定義しています。これにより、メモリリークや不要なタイマーの実行を防げます。


結び

  • マウント時: コンポーネントが初めてレンダリングされた後に一度だけ副作用を実行します。useEffectの依存配列を空にすることで実現できます。
  • 更新時: 依存配列に指定した変数が変更された際に副作用を再実行します。これにより、データの変化に応じた処理を行うことができます。
  • アンマウント時: コンポーネントが破棄される前にクリーンアップ処理を行います。これにより、不要なタイマーやリソースの解放を行うことができます。

4. 主なユースケース(データ取得、DOM操作、外部ライブラリとの連携)

useEffect は、副作用を管理するための強力なツールですが、その用途には多くのケースがあります。 以下のセクションでは、実際のユースケースを詳しく解説し、useEffect を使った実践的なアプローチを深掘りします。

1. データのフェッチ(APIからのデータ取得)

React アプリケーションでは、API からデータを取得して表示することがよくあります。

useEffect を使うと、コンポーネントの初回レンダリング後にデータを非同期で取得し、ステートに保存することができます。

例: APIからデータを取得して表示する

import { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // データを非同期でフェッチ
    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((json) => {
        setData(json);
        setLoading(false);
      })
      .catch((error) => {
        console.error('Error fetching data:', error);
        setLoading(false);
      });
  }, []); // 空の配列:最初のレンダリング時のみ実行

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

  return (
    <div>
      <h1>Fetched Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}
  • ポイント:
    useEffect 内で 非同期処理(fetch)を行い、その結果を useState で管理します。useEffect の依存配列に空の配列 [] を指定することで、コンポーネントが最初にレンダリングされたときに一度だけデータを取得するようになります。

  • APIフェッチの流れ:

    • コンポーネントがマウントされると、useEffect 内で fetch が実行され、データが非同期に取得されます。
    • データが取得された後に、setData を使ってステートを更新し、再レンダリングされます。
    • フェッチが終了するまで loading ステートでローディングメッセージを表示します。

2. DOMの操作や外部ライブラリとの連携

React では、通常、DOM 操作を避けるべきですが、場合によっては外部ライブラリやフレームワークとの連携が必要になることがあります。 useEffect を使って、コンポーネントのライフサイクルに合わせて外部ライブラリを初期化したり、DOMの操作を行うことができます。

例: 外部ライブラリの初期化

例えば、Google Maps API を使って地図を表示したい場合、地図の初期化を useEffect で行うことができます。

import { useEffect } from 'react';

function GoogleMap() {
  useEffect(() => {
    // Google Maps APIの初期化
    const map = new window.google.maps.Map(document.getElementById("map"), {
      center: { lat: -34.397, lng: 150.644 },
      zoom: 8,
    });

    // ここでさらにマーカーやイベントリスナーを追加することもできます

  }, []); // 初回レンダリング後のみ実行

  return <div id="map" style={{ width: "100%", height: "400px" }}></div>;
}
  • ポイント: 外部ライブラリのセットアップ(ここでは Google Maps)を useEffect 内で行い、依存配列を空にすることで、コンポーネントがマウントされた後に一度だけ実行されるようにしています。
  • Google Maps API のような外部ライブラリを使う場合、DOMの操作や外部サービスとの連携が必要です。これも useEffect で副作用として管理できます。

3. タイマーやインターバルの管理

useEffect を使って、タイマーやインターバル処理を管理することもよくあります。たとえば、setIntervalsetTimeout を使って時間間隔で処理を実行したり、カウントダウンを実装することができます。

例: タイマーの設定とクリーンアップ

import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000); // 1秒ごとにカウントアップ

    // クリーンアップ処理:コンポーネントがアンマウントされた時にタイマーをクリア
    return () => clearInterval(intervalId); // アンマウント時に実行される
  }, []); // 初回レンダリング後のみ実行

  return <div>Time: {seconds}s</div>;
}
  • ポイント: setInterval を使って、コンポーネントが表示されてから1秒ごとに秒数をカウントアップします。useEffect のクリーンアップ関数(return () => clearInterval(intervalId))を使って、コンポーネントがアンマウントされたときにタイマーをクリアし、メモリリークを防いでいます。

結び

useEffect は、以下のような 副作用の処理 を管理するために使用されます:

  1. データ取得:
     APIからデータを取得し、ステートに保存する。

  2. DOM操作や外部ライブラリとの連携:
     Google Maps API などの外部ライブラリを使用して、コンポーネントのライフサイクルに基づいて初期化や設定を行う。

  3. タイマーやインターバルの管理:
     setInterval や setTimeout を使って、一定間隔で処理を実行する。

副作用を適切に管理するために、useEffect を効果的に使用することで、Reactアプリケーションのパフォーマンス向上やバグの予防ができます。

5. 依存配列(deps)とその振る舞い

useEffect における**依存配列(Dependency Array)**は、コンポーネントが再レンダリングされるタイミングを制御する非常に重要な部分です。

この配列を使うことで、副作用が実行される条件を細かく指定できます。依存配列の理解は、useEffect の効果的な使用法に不可欠です。


1. 依存配列とは?

useEffect の第二引数として渡される配列が依存配列です。この配列に指定された**変数(依存関係)**が変化したときに、useEffect 内の副作用が実行されます。

useEffect(() => {
  // 副作用の処理
}, [依存関係]);

2. 空の依存配列 [] の使い方

空の依存配列([])を指定した場合、useEffect はコンポーネントがマウントされた後に一度だけ実行され、依存関係がないため再実行されることはありません。

例: 初回レンダリング後に一度だけ実行する場合

useEffect(() => {
  console.log("コンポーネントが初回レンダリングされたときに一度だけ実行");
}, []); // 空の依存配列:最初のレンダリング後に一度だけ実行
  • この場合、コンポーネントが最初に表示された時のみ、useEffect の中の処理が実行されます。再レンダリングが起きても、useEffect は再実行されません。

3. 特定の変数が変化したときに実行

依存配列に特定の変数を渡すと、その変数が変更されるたびに副作用が再実行されます。これを使って、特定の状態やプロパティが更新されたときに副作用を実行することができます。

例: id が変わったときに再実行される副作用

useEffect(() => {
  console.log(`idが変更されました: ${id}`);
}, [id]); // idが変わったときに再実行される
  • 依存配列にidを指定した場合、idが変更されるたびにuseEffect内の処理が実行されます。これを使って、データの再取得やUIの更新を行います。

4. 複数の依存関係

依存配列には複数の変数を指定することもできます。その場合、依存配列内のいずれかの変数が変更された場合に、useEffectが再実行されます。

例: 複数の依存関係に基づいて副作用を実行

useEffect(() => {
  console.log(`nameが変更されました: ${name}`);
  console.log(`ageが変更されました: ${age}`);
}, [name, age]); // nameまたはageが変わったときに実行される
  • この場合、name または age が変更されるたびに副作用が実行されます。複数の状態やプロパティに依存する処理に有効です。

5. 依存関係の比較と最適化

依存配列内に指定された変数が変更されると、React は前回と現在の値を比較して、再実行する必要があるかどうかを判断します。依存関係の最適化にはいくつかの注意点があります。

1. 依存関係の比較:浅い比較

React は、依存配列内の値を浅い比較(shallow comparison)で判断します。つまり、オブジェクトや配列が依存関係に含まれている場合、その内容が変わったかどうかを参照の変更で比較します。

例: オブジェクトを依存関係に指定する場合
const user = { name: 'Alice' };

useEffect(() => {
  console.log('userが変更されました');
}, [user]); // userオブジェクトの参照が変更されたときに実行
  • user オブジェクト自体の内容(name の変更など)は比較されません。もしオブジェクトの内容が変わっただけで user の参照が変わらなければ、useEffect は再実行されません。これにより、オブジェクトの内容が変更されても再実行されない場合があります。

2. 配列を依存関係に指定する場合

配列も同様に、参照の変化を基準に再実行されます。配列の内容が変わっても、配列自体の参照が変わらなければ useEffect は再実行されません。

例: 配列を依存関係に指定する場合
const items = [1, 2, 3];

useEffect(() => {
  console.log('itemsが変更されました');
}, [items]); // items配列の参照が変更されたときに実行
  • 配列の中身が変わったとしても、items 配列の参照が変更されていない限り、useEffect は再実行されません。

6. 依存配列の落とし穴:無限ループ

依存配列を誤って設定すると、無限ループに陥ることがあります。特に、useEffect 内で状態を更新する処理を行い、その状態を依存配列に指定してしまうと、状態更新のたびに再レンダリングが発生し、useEffectが無限に実行され続けることがあります。

例: 無限ループの発生

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

useEffect(() => {
  setCount(count + 1); // countが変更される度に実行される
}, [count]); // countが変更される度に実行され、無限ループが発生
  • 上記のように、setCount を使って count を更新し、その count を依存配列に指定してしまうと、毎回状態が更新され、useEffect が無限に実行されることになります。

無限ループを避ける方法

無限ループを防ぐためには、useEffect 内で状態更新を行う場合、その依存配列に状態更新処理を含めないように注意します。

修正案: 条件を追加して無限ループを防ぐ

useEffect(() => {
  if (count < 5) {
    setCount(count + 1);
  }
}, [count]); // 条件を追加して無限ループを防ぐ

結び

  • useEffect と依存配列をうまく使うことで、副作用を効率的に管理できます。
  • 空の配列 [] を使うことで、副作用を初回レンダリング時に一度だけ実行できます。
  • 特定の変数の変更時に副作用を再実行したい場合は、その変数を依存配列に指定します。
  • 無限ループを防ぐため、依存配列の設定に注意が必要です。特に、状態更新を行う場合は、無駄な再実行を防ぐための工夫が求められます。

6. クリーンアップ機能(return () => …)とメモリリーク回避

useEffect のもう一つの重要な機能は クリーンアップ処理 です。副作用の処理が実行された後、コンポーネントがアンマウント(破棄)されたり、依存関係が変更されるたびにクリーンアップ処理を行うことができます。これにより、メモリリークを防ぎ、アプリケーションのパフォーマンスを最適化できます。


1. クリーンアップの役割

Reactでは、コンポーネントがアンマウントされた後や依存関係が変わったときに、行った副作用の処理をクリーンアップする必要があります。そうしないと、不要なメモリ使用やタイマー、イベントリスナーが残ってしまい、メモリリーク予期しない挙動を引き起こす可能性があります。

クリーンアップ処理は、useEffectの戻り値として関数を返すことで実行できます。これにより、コンポーネントがアンマウントされる前や、次回の副作用が実行される前に、不要なリソースを解放することができます。

2. クリーンアップ関数の基本構文

useEffect内で戻り値として関数を返すと、その関数がクリーンアップ関数として扱われます。このクリーンアップ関数は、コンポーネントのアンマウント時依存関係が変わる前に実行されます。

useEffect(() => {
  // 副作用の処理(例:イベントリスナーの追加、タイマーの設定など)

  return () => {
    // クリーンアップ処理(例:イベントリスナーの削除、タイマーのクリアなど)
  };
}, [依存関係]);

3. クリーンアップ処理の実践例

useEffectのクリーンアップを活用して、よく使われるケースを見ていきましょう。

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

外部イベント(スクロールやキーボード入力など)に反応する処理では、イベントリスナーを追加してその後に削除する必要があります。useEffectを使ってイベントリスナーを設定し、クリーンアップ時にそれを削除する方法を見てみましょう。

import { useEffect } from 'react';

function MouseTracker() {
  useEffect(() => {
    const handleMouseMove = (event) => {
      console.log(`Mouse position: (${event.clientX}, ${event.clientY})`);
    };

    // マウスムーブイベントを追加
    window.addEventListener('mousemove', handleMouseMove);

    // クリーンアップ:イベントリスナーを削除
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // 初回レンダリング時のみ実行

  return <div>Move your mouse around!</div>;
}
  • ポイント: useEffect 内で window.addEventListener を使ってマウスの位置を追跡し、コンポーネントがアンマウントされる時にクリーンアップ関数でリスナーを削除します。これにより、不要なリスナーが残り、メモリリークを引き起こすことを防ぎます。

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

setIntervalsetTimeout を使ってタイマーを設定する場合も、コンポーネントがアンマウントされた時にタイマーをクリアする処理が必要です。

import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000); // 1秒ごとにカウントアップ

    // クリーンアップ:コンポーネントがアンマウントされた時にタイマーをクリア
    return () => clearInterval(intervalId);
  }, []); // 初回レンダリング後のみ実行

  return <div>Seconds: {seconds}</div>;
}
  • ポイント: タイマーを設定した後、コンポーネントがアンマウントされた際に clearInterval を呼び出してタイマーをクリアします。これにより、アンマウント後にもタイマーが動作し続けることがなくなります。

4. メモリリークの防止

useEffect のクリーンアップ関数を適切に使うことで、メモリリークを防ぎ、アプリケーションのパフォーマンスを最適化できます。特に、APIの呼び出しや、タイマー、イベントリスナー、サードパーティライブラリのリソースなどを使った場合には、クリーンアップ処理を忘れずに行いましょう。

メモリリークが発生するシナリオ

  • イベントリスナーが削除されない
  • タイマーがクリアされない
  • サードパーティライブラリでのリソース解放忘れ

これらを防ぐためには、useEffect でクリーンアップ関数を返すことを習慣化しましょう。


5. クリーンアップ関数を使った実践的なユースケース

クリーンアップ関数は、他にもさまざまなケースで活用できます。以下にいくつかの例を挙げてみます。

例: WebSocketのクリーンアップ

WebSocket を使う場合、接続の解除をクリーンアップ関数で行う必要があります。

import { useEffect } from 'react';

function WebSocketComponent() {
  useEffect(() => {
    const socket = new WebSocket('ws://example.com/socket');

    socket.onmessage = (event) => {
      console.log('Message from server:', event.data);
    };

    // クリーンアップ:WebSocket接続を閉じる
    return () => {
      socket.close();
    };
  }, []); // 初回レンダリング時に一度だけ実行

  return <div>Listening for WebSocket messages...</div>;
}

例: サブスクリプションの解除

サードパーティのライブラリやサービスを使って、データのサブスクリプションを管理する場合にも、クリーンアップ処理が必要です。

import { useEffect } from 'react';

function DataSubscription() {
  useEffect(() => {
    const unsubscribe = subscribeToData((data) => {
      console.log('Received data:', data);
    });

    // クリーンアップ:サブスクリプションの解除
    return () => {
      unsubscribe();
    };
  }, []); // 初回レンダリング時に一度だけ実行

  return <div>Subscribed to data...</div>;
}

結び

  • クリーンアップ処理は、useEffect で副作用の後処理を行い、メモリリークや不要なリソースの消費を防ぐために非常に重要です。
  • イベントリスナーやタイマーの解除サードパーティライブラリとの連携など、リソースを解放する処理を忘れないようにすることが、パフォーマンスを最適化するためのカギとなります。
  • useEffect 内で戻り値として関数を返すことで、コンポーネントのアンマウントや依存関係が変わるタイミングで、必要なクリーンアップ処理を実行できます。

7. ベストプラクティスと落とし穴

useEffect は非常に便利で強力なフックですが、使い方によっては パフォーマンスの低下バグを引き起こす原因になることもあります。ここでは、useEffect を効果的に使うための ベストプラクティスと、避けるべき 落とし穴について詳しく解説します。

1. ベストプラクティス

1.1 複数の useEffect を使う

React コンポーネント内で複数の副作用がある場合、複数の useEffect を使うことを推奨します。これにより、関心ごとが明確になり、コードがシンプルで理解しやすくなります。

  • 1つの useEffect 内で複数の副作用を管理するよりも、役割ごとに useEffect を分ける方が可読性が向上します。
例: 複数の副作用を分けて使う
useEffect(() => {
  // APIからデータを取得
  fetchData();
}, []); // 初回レンダリング時に実行

useEffect(() => {
  // イベントリスナーを設定
  window.addEventListener('resize', handleResize);

  return () => {
    // クリーンアップ:イベントリスナーの解除
    window.removeEventListener('resize', handleResize);
  };
}, []); // 初回レンダリング時に実行
  • この方法では、データのフェッチとイベントリスナーの管理を別々に行い、それぞれの副作用が何をしているのかが明確になります。

1.2 必要な依存関係のみ指定

useEffect の依存配列に必要な依存関係だけを指定することが重要です。すべてのステートやプロパティを依存関係に含めると、不必要な再実行が発生してしまう可能性があります。

  • 依存配列に 必要な依存関係だけ を含め、再レンダリングを最小限に抑えましょう。
例: 最小限の依存関係
useEffect(() => {
  console.log('Data updated');
}, [data]); // `data` が変更されたときだけ実行
  • data が変更されたときにのみ useEffect が実行され、無駄な再実行を防ぎます。

1.3 関数コンポーネント内でのステート更新を避ける

useEffect 内で状態を更新する際には、過剰な状態更新を避けることが重要です。useEffect の依存配列に指定されたステートが変更されるたびに副作用が実行されるため、状態更新を繰り返し行うと、無限ループに陥る可能性があります。

例: 状態更新を条件付きで行う
useEffect(() => {
  if (data !== previousData) {
    setData(data);
  }
}, [data]);
  • data が変更された場合にのみ、setData を呼び出して、無駄な状態更新を避けます。

2. 落とし穴

2.1 依存配列を省略してしまう

useEffect に依存配列を省略すると、コンポーネントが再レンダリングされるたびに副作用が実行されます。これにより、無駄な再実行や、予期しない副作用が発生する可能性があります。

例: 依存配列を省略すると無限ループが発生
useEffect(() => {
  setCount(count + 1); // 状態更新
});
  • 依存配列を指定しない場合、状態更新(setCount)が行われるたびに再レンダリングが発生し、再度 useEffect が実行されます。これが無限に繰り返され、ブラウザが固まる可能性があります。

2.2 無駄な useEffect の再実行

依存関係の設定を誤ると、無駄な再実行が発生することがあります。特に、配列やオブジェクトを依存関係に指定した場合、それらの参照が変更されると再実行されるため、意図しない副作用が発生することがあります。

例: 配列やオブジェクトを依存配列に含める場合
useEffect(() => {
  console.log('Data has changed');
}, [myArray]); // 配列の参照が変更されるたびに実行
  • myArray参照が変更されるたびにuseEffect が実行されるため、配列の内容が変わっただけでは再実行されませんが、参照そのものが変わると無駄に再実行されます。

2.3 クリーンアップの忘れ

クリーンアップ関数(return () => {})を忘れてしまうと、イベントリスナーやタイマーが残ったままになり、メモリリーク不安定な状態が発生する原因になります。

例: クリーンアップを忘れる
useEffect(() => {
  const intervalId = setInterval(() => {
    console.log('Timer is running');
  }, 1000);
}, []); // クリーンアップ関数なし
  • コンポーネントがアンマウントされた後も setInterval が実行され続け、メモリリークが発生します。クリーンアップ処理を忘れずに実装しましょう。

3. ベストプラクティスをまとめる

  • 複数の useEffect を使い、関心ごとを分けることで、コードの可読性とメンテナンス性が向上します。
  • 依存配列を慎重に設定し、必要な依存関係のみを指定することで、無駄な副作用の再実行を防ぎます。
  • 状態更新を条件付きで行うことによって、無駄な状態変更を避け、パフォーマンスを最適化します。
  • クリーンアップ関数を忘れずに実装し、メモリリークを防ぐことが重要です。

結び

8. 実践コード例(簡単な API 呼び出し+データ表示コンポーネント)

これまでに学んだ useEffect の使い方を実践的に適用するために、以下では実際のコード例を使って APIからデータをフェッチして表示する 例を紹介します。

この例を通じて、React コンポーネントの中での副作用の使い方や、実際のアプリケーションにおける useEffect の利用方法が具体的に理解できるようになります。


1. データのフェッチと表示の基本例

ReactでAPIからデータを取得して、取得したデータを表示する基本的なパターンを見ていきましょう。

例: シンプルな API 呼び出しコンポーネント

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

function FetchDataComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // APIからデータをフェッチする処理
    fetch('https://api.example.com/data')
      .then((response) => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then((json) => {
        setData(json); // データの保存
        setLoading(false); // ローディング完了
      })
      .catch((err) => {
        setError(err.message); // エラーメッセージを保存
        setLoading(false); // ローディング完了
      });
  }, []); // 空の依存配列で初回マウント時にのみ実行

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

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

  return (
    <div>
      <h1>Fetched Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default FetchDataComponent;

コード解説

  • useState: dataloadingerrorの3つのステートを管理します。data はAPIから取得したデータ、loading はデータ取得中かどうか、error はエラーメッセージを保存します。
  • useEffect: コンポーネントが最初にマウントされたときに、fetch を使ってデータを取得します。データ取得中は loadingtrue に設定し、データ取得後に loadingfalse に、エラーが発生した場合にはエラーメッセージを error ステートに設定します。
  • 依存配列 []: 空の依存配列を指定しているため、useEffect は初回レンダリング後に一度だけ実行されます。

2. 誤ったデータの取得と再実行

useEffect が適切に動作するように、依存配列を使用して、どのタイミングで副作用を再実行するかを設定することが重要です。次に、データが変更されるたびに再度APIを呼び出す例を紹介します。

例: id が変更されるたびにAPIを再呼び出し

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

function FetchUserData({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    setError(null);

    // APIからデータをフェッチする処理
    fetch(`https://api.example.com/users/${userId}`)
      .then((response) => response.json())
      .then((json) => {
        setData(json); // データを保存
        setLoading(false); // ローディング終了
      })
      .catch((err) => {
        setError(err.message); // エラーメッセージを保存
        setLoading(false); // ローディング終了
      });
  }, [userId]); // userIdが変更されるたびに再実行

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

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

  return (
    <div>
      <h1>User Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default FetchUserData;

コード解説

  • userId が変更されるたびにデータを再取得: このコンポーネントは、userId が変更されるたびにデータを再取得する必要があります。依存配列に userId を指定することで、userId が変更されるたびに副作用が再実行されます。
  • useEffect の依存配列に userId を追加: userId が変更されるたびに useEffect が再実行され、その度に新しいユーザーデータを取得します。

3. 非同期関数の扱いとクリーンアップ

useEffect 内で非同期処理を行う際には、非同期関数(async を直接 useEffect のコールバック関数内に書くことができません。しかし、async 関数を呼び出す関数を作成し、それを useEffect 内で呼び出すことは可能です。

また、非同期処理中にコンポーネントがアンマウントされる場合に備え、クリーンアップ処理を追加することも重要です。

例: 非同期関数とクリーンアップ

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

function FetchDataWithCleanup() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true; // コンポーネントがマウントされているかどうかを追跡

    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const json = await response.json();
        if (isMounted) {
          setData(json);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // クリーンアップ: コンポーネントがアンマウントされたときに処理をキャンセル
    return () => {
      isMounted = false; // コンポーネントがアンマウントされたことを示す
    };
  }, []); // 最初のレンダリング時のみ実行

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

  return <div>{JSON.stringify(data, null, 2)}</div>;
}

export default FetchDataWithCleanup;

コード解説

  • isMounted フラグ: 非同期処理中にコンポーネントがアンマウントされることを防ぐために、isMounted フラグを使って、コンポーネントがマウントされている場合のみステートを更新します。これにより、コンポーネントがアンマウントされた後にステート更新が行われないようにします。
  • 非同期関数の実行: useEffect 内で非同期関数を実行するために、非同期関数 fetchData を作成し、useEffect 内で呼び出しています。

結び

  • データの取得: useEffect を使って API からデータを取得し、ステートに保存する方法を学びました。副作用をコンポーネントのライフサイクルに合わせて管理できます。
  • 依存関係による再実行: 依存配列を使って、特定の値(例えば userId)が変更されたときに副作用を再実行する方法を学びました。
  • 非同期関数とクリーンアップ: 非同期処理を行う際には、クリーンアップ処理を忘れずに実装し、アンマウント後の状態更新を防ぐ方法を学びました。

9. まとめ & 次回予告

この記事では、ReactのuseEffectフックを使った副作用の管理方法について、基本的な使い方から、実際のユースケース、ライフサイクルとの関係、依存配列、クリーンアップ処理、そしてベストプラクティスまでを深掘りしてきました。ここで得た知識を基に、Reactコンポーネントのパフォーマンスや可読性を向上させることができます。

  1. useEffectの基本的な使い方
    useEffectは副作用を管理するためのReactフックです。コンポーネントのライフサイクルに合わせて副作用を実行したり、クリーンアップするために利用します。

  2. 副作用の管理

    • 初回レンダリング後に一度だけ実行する場合、空の依存配列 [] を指定します。
    • 依存配列に変数を指定することで、その変数が変化したときに再実行されます。
    • 無駄な再実行を避けるため、依存配列には必要な変数だけを指定することが重要です。
  3. クリーンアップ処理
    useEffectでは、戻り値としてクリーンアップ関数を返すことで、コンポーネントがアンマウントされる前や依存関係が変わる前にリソースを解放することができます。これにより、メモリリークを防ぐことができます。

  4. ベストプラクティス

    • 複数のuseEffectを使って副作用を分けることで、コードの可読性と管理がしやすくなります。
    • 必要な依存関係だけを依存配列に指定し、無駄な再実行を避けるようにしましょう。
    • 状態更新を条件付きで行い、無限ループや不必要な再レンダリングを防ぐようにします。
  5. 落とし穴と回避策

    • 依存配列を省略したり、適切に設定しないと無限ループや不必要な再実行が発生します。
    • イベントリスナーやタイマー、外部リソースとの連携を行った場合は、必ずクリーンアップ処理を追加して、メモリリークを防ぎましょう。

過去記事