【Reactフック】useEffectの使い方と活用法

JavaScript

React.jsは、そのシンプルさと強力な機能で、多くの開発者に愛されています。その中でも特に重要な要素の一つが「フック」です。フックは、関数コンポーネントで状態やライフサイクル機能を利用するための仕組みであり、Reactをより強力かつ柔軟に使いこなすために欠かせないものです。

以前の記事で、useStateフックについて学びましたが、Reactには他にも多くのフックが存在します。この記事ではuseState以外のReactフックの中から、まずはuseEffectを紹介し、使い方と効果的な利用方法について解説します。

useEffectとは

ReactのuseEffectフックは、副作用の処理に使用されます。
副作用とは、外部(APIやデータベース、ファイルシステムなど(※1))からのデータの取得、サブスクリプション(※2)の設定、DOMの更新など、レンダリングの結果として発生する操作のことです。

useEffectフックを使用することで、コンポーネントのライフサイクルに基づいてこれらの操作を実行できます。

※1)ここでいう外部には、前述のAPIやデータベース、ファイルシステム以外に次のものもあります。

  • WebSocket
  • Local StorageやSession Storage
  • アプリケーションのキャッシュ(IndexedDBなど)
  • ServiceWorker

※2)ここでいう「サブスクリプション」とは、通常の「購読」、いわゆる「サブスク」とは少々異なります。

技術的な文脈でのサブスクリプションは、特定のイベントが発生したときにそれに応じて何かを実行する仕組みです。

例えば、WebSocketやイベントリスナーは典型的なサブスクリプションの例です。

基本的な使い方

useEffectフックの基本的な使い方を以下のコードで確認してみましょう。

サンプルコード
import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

上記のサンプルでは、カウントが変更されるたびにドキュメントのタイトルが更新されます。
useEffectフックの第二引数には依存関係の配列を指定でき、ここではcountが変更されたときのみ副作用が再実行されます。

あるstateやpropsの変更により当事者以外のデータを更新したい場合には、useEffectを使用します。

そうすることで、依存関係(例えば、特定のstateやpropsの変更)が発生したときにのみ副作用を実行できるため、無駄な更新や無限ループを未然に防止しやすくなります。

また、副作用の処理をコンポーネントの主要なロジックから分離できるため、コードの可読性と保守性が向上します。

よくある使い方と実践的なサンプル

useEffectフックは様々なシナリオで役立ちます。以下にいくつかの実践的な例を示します。

データの取得

APIからデータを取得する際にuseEffectを使用するサンプルです。

サンプルコード
import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, []);

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

  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

このサンプルでは、コンポーネントが初めてレンダリングされた時にAPIからデータを取得し、dataの状態(state)を更新しています。
依存関係の配列が空のため、useEffectは一度だけ実行されます。

なお、プログラム内で使用されているURLは実在していません。

サブスクリプションの設定

WebSocketのようなサブスクリプションを設定し、クリーンアップを行うサンプルです。

サンプルコード
import React, { useState, useEffect } from 'react';

function WebSocketComponent() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket('wss://example.com/socket');
    ws.onmessage = (event) => {
      setMessages(prevMessages => [...prevMessages, event.data]);
    };

    return () => {
      ws.close();
    };
  }, []);

  return (
    <ul>
      {messages.map((msg, index) => (
        <li key={index}>{msg}</li>
      ))}
    </ul>
  );
}

このサンプルでは、WebSocket接続を設定し、メッセージを受信するたびにstateを更新しています。useEffectのクリーンアップ関数を使用して、コンポーネントがアンマウントされる際にWebSocket接続をクローズします。

ベストプラクティスと注意点

依存関係の配列を適切に設定する

useEffectフックの依存関係の配列は、エフェクトの再実行条件を指定するために重要な役割を果たします。
依存関係の配列を適切に設定することで、エフェクトが意図したタイミングで実行され、パフォーマンスの最適化や予期しない再レンダリングの防止ができます。

依存関係の配列を適切に設定するためのポイントが5つあります。

1. 必要な依存関係を全て指定する

useEffectの中で使用されるすべての変数やプロパティを依存関係として指定しましょう。そうすることで、依存関係が変更されたときにエフェクトが再実行されます。

2. 複数の依存関係を指定する

useEffectの中で複数の変数やプロパティが使われる場合は、すべてを依存関係として指定する必要があります。これにより、それぞれの依存関係が変更されるたびにエフェクトが再実行されます。

useEffect(() => {
  console.log(`Count: ${count}, Name: ${name}`);
}, [count, name]);

3. コンポーネントの再レンダリングを最小限に抑える

不必要な再レンダリングを防ぐために、依存関係の配列に過剰な変数を含めないようにしましょう。必要な依存関係だけを指定することが重要です。

4. 特定のタイミングでのみエフェクトを実行する

依存関係の配列を空([])にすると、エフェクトはコンポーネントのマウント時に一度だけ実行されます。これは、一度きりの初期化やサブスクリプションの設定に適しています。

5. オブジェクトや配列を依存関係として扱う場合の注意

オブジェクトや配列を依存関係として指定する場合は注意が必要です。それらが毎回新しい参照を持つため、意図しないエフェクトの再実行が発生することがあります。この場合、特定のプロパティや要素に依存させるか、useMemoやuseCallbackを使用して参照を安定させることが有効です。

オブジェクトの特定のプロパティに依存させる例は次の通りです。

useEffect(() => {
  console.log(`User ID: ${user.id}`);
}, [user.id]);

クリーンアップ関数を忘れない

副作用によって設定されたリソース(例: イベントリスナー、タイマー、サブスクリプションなど)を解放するために、クリーンアップ関数を必ず定義しましょう。

クリーンアップ関数は、リソースが不要になったときにそれを適切に解除して、メモリリークや予期しない動作を防ぎます。

クリーンアップ関数が必要なのは、次のような場合です。

1. イベントリスナーの追加

useEffect(() => {
  const handleResize = () => {
    console.log('Window resized');
  };
  window.addEventListener('resize', handleResize);

  // returnされているのがクリーンアップ関数
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

2. タイマーの設定

useEffect(() => {
  const timerId = setInterval(() => {
    console.log('Timer tick');
  }, 1000);

  // クリーンアップ関数
  return () => {
    clearInterval(timerId);
  };
}, []);

3. WebSocket接続

useEffect(() => {
  const socket = new WebSocket('wss://example.com/socket');
  socket.onmessage = (event) => {
    console.log(event.data);
  };

  // クリーンアップ関数
  return () => {
    socket.close();
  };
}, []);

副作用がリソースを使用しているかどうかを判断する基準として、以下の点に注意してください。

  • 外部システムや環境に影響を与える場合(例: API呼び出し、データベース接続)
  • 非同期処理を伴う場合(例: fetch APIによるデータ取得)
  • リスナーやハンドラーを設定する場合(例: ウィンドウのリサイズイベント)
  • タイマーやアニメーションを使用する場合(例: setInterval、requestAnimationFrame)
  • サブスクリプションを行う場合(例: RxJSのストリーム)

副作用の分割

useEffectフック内で複数の異なる副作用をまとめて処理するのは避け、役割ごとに分割することが推奨されます。
これにより、コードの可読性とメンテナンス性が向上し、各副作用が明確に分かれていることでバグの発生を防ぐことができます。

副作用の分割を行うためのポイントが4つあります。

1. 目的ごとに副作用を分割する

各useEffectフックは、それぞれ異なる目的や役割に対応するべきです。これにより、コードが直感的になり、将来的な変更が容易になります。

useEffect(() => {
  // 目的1:カウントに基づいてドキュメントタイトルを更新する
  document.title = `You clicked ${count} times`;
}, [count]);

useEffect(() => {
  // 目的2:ウィンドウのリサイズイベントをリッスンする
  const handleResize = () => {
    console.log('Window resized');
  };
  window.addEventListener('resize', handleResize);

  // クリーンアップ関数
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

2. 依存関係が異なる副作用を分割する

依存関係が異なる副作用を一つのuseEffectフックでまとめると、意図しない再実行が発生する可能性があります。
それぞれの依存関係に基づいて個別に分割しましょう。

useEffect(() => {
  // countに依存する副作用
  document.title = `You clicked ${count} times`;
}, [count]);

useEffect(() => {
  // nameに依存する副作用
  console.log(`Name is ${name}`);
}, [name]);

3. クリーンアップが必要な副作用を分割する

クリーンアップが必要な副作用とそうでない副作用を分割することで、各副作用のライフサイクルが明確になります。

useEffect(() => {
  // イベントリスナーを追加する副作用
  const handleResize = () => {
    console.log('Window resized');
  };
  window.addEventListener('resize', handleResize);

  // クリーンアップ関数
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

useEffect(() => {
  // 単純なログ出力の副作用
  console.log('Component rendered');
}, []);

まとめ

eactのuseEffectフックは、関数コンポーネント内で副作用を扱うための強力なツールです。
この記事では、useEffectの基本的な使い方とベストプラクティスについて詳しく説明しました。

以下に要点をまとめます。

  1. 基本的な使い方
    • useEffectはコンポーネントがマウントされるときや、依存関係が変更されるたびに実行される関数です。副作用を管理し、コンポーネントのライフサイクルに応じて適切に実行することができます。
  2. クリーンアップ関数
    • クリーンアップ関数を使用することで、リソースの解放やイベントリスナーの解除を行い、メモリリークを防ぐことができます。副作用が外部リソース(例: イベントリスナー、タイマー、WebSocket接続など)を使用する場合には、クリーンアップ関数を忘れずに定義しましょう。
  3. 依存関係の配列
    • useEffectの依存関係の配列は、エフェクトの再実行条件を指定するために重要です。必要な依存関係を全て指定し、不要な再レンダリングを防ぐことで、パフォーマンスを最適化できます。また、依存関係が異なる副作用を分割することで、コードの可読性とメンテナンス性が向上します。
  4. 副作用の分割
    • 複数の異なる副作用をまとめずに、役割ごとに分割することで、各副作用のライフサイクルが明確になり、バグの発生を防ぐことができます。

useEffectフックを適切に活用することで、Reactコンポーネントの動作を効率的に管理し、アプリケーションのパフォーマンスを向上させることができます。

長い記事を最後までお読みいただき、ありがとうございました。

質問、ご意見、ご感想などありましたら、お気軽にコメントいただければ幸いです。

コメント

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