Reactは、ユーザーインターフェースを構築できるJavaScriptライブラリとして高い人気を誇ります。そんなインターフェース構築の際には、APIからデータを取得したり、イベントを購読したり、DOMを操作したりといった副作用を実行する必要があるかもしれません。

そこで便利なuseEffectフックが活躍します。これにより、上記のような副作用を宣言的かつ効率的に、そしてシームレスに処理できるようになり、UIがレスポンシブで最新の状態に保たれます。

Reactの初心者であれ経験豊富な開発者であれ、useEffectを理解し使いこなすことが、堅牢で動的なアプリケーションを構築するために不可欠です。この記事では、useEffectフックの仕組みと、Reactプロジェクトでの使用方法をご紹介します。

Reactにおける副作用とは

Reactコンポーネントを扱っていると、Reactのスコープ外のエンティティとのやり取りやアクションの実行が必要になることがあります。このような外部とのやりとりは、副作用(サイドエフェクトとも)として知られています。

Reactでは、ほとんどのコンポーネントは純粋な関数です。つまり、以下の例に見られるように、入力(props)を受け取り、予測可能な出力(JSX)を生成することになります。

export default function App() {
  return <User userName="JaneDoe" />   
}
  
function User(props) {
  return <h1>{props.userName}</h1>; // John Doe
}

しかし、副作用はReactの通常のスコープ外の相互作用に関わるため、先のようには予測できません。

ブラウザのタブのタイトルを動的に変更して、ユーザーのuserNameを表示したい場合を考えてみましょう。処理をコンポーネント内で直接行いたい気持ちはわかりますが、これは副作用と見なされるため、推奨されるアプローチではありません。

const User = ({ userName }) => {
  document.title = `Hello ${userName}`; // ❌コンポーネント内では非推奨(副作用であるため)

  return <h1>{userName}</h1>;
}

コンポーネント本体内で直接副作用を実行すると、Reactコンポーネントのレンダリング処理に支障をきたす可能性があります。

干渉を避けるには、副作用を分離して、コンポーネントがレンダリングされた後にのみレンダリングまたは機能するようにし、レンダリング処理と必要な外部とのやり取りを明確に分離する必要があります。この分離をuseEffectフックで行います。

useEffectの基本を理解する

useEffectフックは、クラスコンポーネントにあるcomponentDidMountcomponentDidUpdatecomponentWillUnmountのようなライフサイクルメソッドを模倣し設計されています。

useEffectを使用するには、「react」からインポートし、関数コンポーネント内(コンポーネントのトップレベル)で呼び出す必要があります。これは、コールバック関数と任意の配列という2つの引数を取ります。

useEffect(callbackFn, [dependencies]);

これは次のように書くとわかりやすくなります。

useEffect(() => {
  // エフェクトがトリガーされたときに実行されるコード
}, [dependencies]);
  • コールバック関数には、コンポーネントがレンダリングされるとき、または依存値が変更されるときに実行されるコードが記述されます。ここで副作用を実行することになります。
  • 依存関係の配列は、変更を監視する値を指定します。コールバック関数は、この配列のいずれかの値が変更されたときに実行されます。

たとえば、useEffectフック内で適切に副作用を実行するように、前の例を修正することができます。すると次のようになります。

import { useEffect } from 'react';

const User = ({ userName }) => {
  useEffect(() => {
    document.title = `こんにちは、${userName}さん`;
  }, [userName]);
    
  return <h1>{userName}</h1>;   
}

上の例では、コンポーネントがレンダリングされた後、依存関係であるuserNameの値が変更されるたびにuseEffectフックが呼び出されます。

useEffectでの依存関係の操作

依存関係は、useEffectの実行を制御する上で重要な役割を果たします。useEffectフックの第2引数です。

useEffect(() => {
  // エフェクトがトリガーされたときに実行されるコード
}, [dependencies]);

空の依存関係配列[]を使用すると、エフェクトは一度だけ実行され、componentDidMountをシミュレートします。依存関係を指定すると、componentDidUpdateと同様に、特定の値が変更されたときにエフェクトが更新されます。

注意)複雑な依存関係を扱うときには注意が必要です。依存関係の配列に含める値を注意深く選択することで、不要な更新を避けることができます。

依存関係の配列を完全に省略すると、コンポーネントがレンダリングするたびにエフェクトが実行され、パフォーマンスの問題につながる可能性があります。

useEffect(() => {
  // エフェクトがトリガーされたときに実行されるコード
});

Reactでは、依存配列の重要性を知ることができるため、レンダリングの仕組みを理解することが大きなプラスになります。

Reactにおけるレンダリングの仕組み

Reactでは、レンダリングはコンポーネントの状態とpropsに基づいて行われ、その結果ユーザーインターフェース(UI)が生成されます。レンダリングが発生する状況はさまざまです。最初のレンダリングは、コンポーネントが最初にレンダリングまたはマウントされることで発生します。

これとは別に、コンポーネントのstatepropsが変更されると、UIに更新後の値が反映されるように再レンダリングが行われます。Reactアプリケーションは、コンポーネントのツリー状の構造で構築され、これが階層を形成します。Reactでは、レンダリング時にルートコンポーネントを開始点として、再帰的に子コンポーネントをレンダリングしていきます。

つまり、ルートコンポーネントに変更が発生すると、すべてのコンポーネントがレンダリングされることになります。注意すべき点として、レンダリングのたびに副作用(ほとんどの場合、リソース消費の激しい関数)を呼び出すと非効率的です。パフォーマンスを最適化するには、useEffectフックの依存配列を使用して、いつトリガーすべきかを指定し、不要な再レンダリングを制限することができます。

useEffectの高度な使い方─副作用の整理

useEffectフックでは、副作用を実行するだけでなく、副作用をクリーンアップすることもできます。これにより、副作用の間に作成されたリソースや購読が適切に解放され、メモリリークの防止が可能になります。

useEffectフックを使用して、どのように副作用をクリーンアップできるかを見てみましょう。

useEffect(() => {
  // 何らかの副作用の実行

  // 副作用のクリーンアップ
  return () => {
    // タスクのクリーンアップ
  };
}, []);

上記のコードでは、useEffectフック内の戻り値としてクリーンアップ関数を定義しています。この関数は、コンポーネントがアンマウントされるときや、その後の再レンダリングの前に呼び出されます。これにより、副作用の間に確立されたリソースや購読をクリーンアップできます。

以下は、useEffectフックの高度な使用例です。

1. 間隔のクリア

useEffect(() => {
    const interval = setInterval(() => {
        // 繰り返されるアクションの実行
    }, 1000);
    return () => {
        clearInterval(interval); // 間隔のクリア
    };
}, []);

この例では、1秒ごとにアクションを実行するインターバルを設定します。クリーンアップ関数が、コンポーネントがアンマウントされた後に実行されないように、インターバルをクリアする役割を果たします。

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

useEffect(() => {
    const handleClick = () => {
        // クリックイベントの処理
    };

    window.addEventListener('click', handleClick);

    return () => {
        window.removeEventListener('click', handleClick); // イベントリスナのクリーンアップ
    };
}, []);

ここで行っているのは、ウィンドウオブジェクトのクリックイベントのイベントリスナを作成することです。クリーンアップ関数が、メモリリークを回避し適切なクリーンアップを保証するために、イベントリスナを削除しています。

クリーンアップ関数は任意ですが、健全で効率的なアプリケーションを維持するために、すべてのリソースまたは購読をクリーンアップすることを強くお勧めします。

useEffectフックの使用

useEffectフックを使用すると、localStorageのようなウェブAPIや外部データソースなど、外部のエンティティやAPIとやり取りするタスクを実行できます。

useEffectフックの使い方を、いろいろなパターンで試してみましょう。

1. ウェブAPIとの連携(localStorage)

useEffect(() => {
 // localStorageにデータを保存
  localStorage.setItem('key', 'value');
  // localStorageからデータを取得
  const data = localStorage.getItem('key');
  // クリーンアップ:コンポーネントのアンマウント時にlocalStorageをクリア
  return () => {
    localStorage.removeItem('key');
  };
}, []);

この例では、useEffectフックを使ってブラウザのlocalStorageにデータを保存したり、そこからデータを取り出したりしています。クリーンアップ関数により、コンポーネントがアンマウントされたときにlocalStorageがクリアされる仕組みです(ブラウザが再読み込みされるまでlocalStorageのデータを保持したいケースもあるため、これは必ずしも優れた使用例とは言えません)。

2. 外部APIからのデータ取得

useEffect(() => {
  // 外部APIからデータを取得
  fetch('https://api.example.com/data')
    .then((response) => response.json())
    .then((data) => {
      // データで何かをする
    });
}, []);

ここでは、useEffectフックを使って外部APIからデータを取得しています。取得したデータは、コンポーネント内で処理して使用することができます。常にクリーンアップ関数を追加する必要はありません。

その他の一般的な副作用

useEffectフックは、他にも以下のような様々な副作用に使用できます。

A. イベントの購読

useEffect(() => {
  window.addEventListener('scroll', handleScroll);
  return () => {
    window.removeEventListener('scroll', handleScroll);
  };
}, []);

B. 文書タイトルの変更

useEffect(() => {
  document.title = 'New Title';
  return () => {
    document.title = 'Previous Title';
  };
}, []);

C. タイマーの管理

useEffect(() => {
  const timer = setInterval(() => {
    // 何かの繰り返し処理を実行
  }, 1000);
  return () => {
    clearInterval(timer);
  };
}, []);

よくあるuseEffectエラーとその回避方法

ReactでuseEffectフックを使っていると、予期せぬ動作やパフォーマンスの問題につながるエラーに遭遇することがあります。

そのようなエラーを理解し、回避方法を知っておくことで、useEffectをスムーズに使用することができます。

それでは、よくあるuseEffectのエラーとその解決策についてご紹介します。

1. 依存配列の欠如

よくある間違いのひとつが、useEffectフックの第2引数に依存配列を入れ忘れることです。

過剰な再レンダリングやデータが古くなるなど、意図しない動作につながる可能性があるため、ESLintは常にこれに対して警告フラグを立てます。

useEffect(() => {
  // 副作用のコード
}); // 依存配列の欠如

解決策useEffectでは(たとえそれが空であっても)常に依存関係の配列を指定してください。エフェクトが依存するすべての変数または値をこれに含めます。Reactがエフェクトを実行するタイミングやスキップするタイミングを判断する上で重要です。

useEffect(() => {
  // 副作用のコード
}, []); // 空の依存配列、または依存配列の中身を指定

2. 不適切な依存関係の配列

依存関係の配列が正しくない場合も、問題につながる可能性があります。依存関係の配列が正確に定義されていないと、期待される依存関係が変更されたときに副作用が実行されない可能性があります。

const count = 5;
const counter = 0;
useEffect(() => {
  // 'count'に依存する副作用コード
  let answer = count + 15;
}, [count]); // 不適切な依存配列

解決策:配列に必要な依存関係をすべて記述するようにしてください。副作用が複数の変数に依存する場合は、依存関係のいずれかが変更されたときにそれがトリガーとなるように、すべての変数を含めます。

const count = 5;
useEffect(() => {
  // 'count'に依存する副作用コード
  let answer = count + 15;
}, [count]); // 適切な依存配列

3. 無限ループ

副作用が、副作用自身にも依存するstateやpropsを変更する場合、無限ループが発生する可能性があります。さらに副作用が繰り返しトリガーされ、過剰な再レンダリングを引き起こし、アプリケーションがフリーズする可能性も考えられます。

const [count, setCount] = useState(0);
useEffect(() => {
  setCount(count + 1); // 副作用内の依存関係’count'の変更
}, [count]); // 依存配列に'count'が含まれる

解決策:副作用が、依存配列に含まれる依存関係を直接変更しないようにする必要があります。代わりに、別の変数を作成するか、他のstate管理のテクニックを使用して、変更内容を処理するようにしてください。

const [count, setCount] = useState(0);
useEffect(() => {
  setCount((prevCount) => prevCount + 1); // コールバックを使った’count'の変更
}, []); // ‘count’の依存を安全に削除できる

4. クリーンアップ忘れ

副作用のクリーンアップを怠ると、メモリリークや不必要なリソース消費につながります。イベントリスナー、インターバル、購読をクリーンアップしないと、特にコンポーネントがアンマウントされたときに、予期しない動作になる可能性があります。

useEffect(() => {
  const timer = setInterval(() => {
    // 何らかの動作を繰り返し実行
  }, 1000);
  // クリーンアップなし
  return () => {
    clearInterval(timer); // return文にクリーンアップがない
  };
}, []);

解決策useEffectフックのreturn文で常にクリーンアップ関数を用意する。

useEffect(() => {
  const timer = setInterval(() => {
    // 何らかの動作を繰り返し実行
  }, 1000);
  return () => {
    clearInterval(timer); // return分にクリーンアップを含む
  };
}, []);

今回ご紹介したよくあるuseEffectエラーを認識し、推奨される解決策に従うことで、潜在的な落とし穴を回避し、ReactアプリケーションでuseEffectフックを正しく効率的に使用することができます。

まとめ

ReactのuseEffectフックは、関数コンポーネントの副作用の管理に便利な機能です。useEffectを深く理解したところで、その知識をReactアプリケーションに応用してみてください。

そして、KinstaのアプリケーションホスティングをReactアプリケーションのデプロイに活用することもお忘れなく。

useEffectフックの活用方法や使いやすさについてご意見がございましたら、お気軽に以下のコメント欄でお聞かせください。