useEffectでサイドエフェクトを扱おう

学習の目標

本章では、以下の内容を学習します。

  • サイドエフェクトの基本概念と必要性を理解する
  • useEffectフックの基本的な使い方を習得する
  • 依存配列の概念と使い方を理解する
  • API通信を使った外部データの取得方法を学ぶ
  • タイマー処理とクリーンアップの方法を習得する
  • useEffectの適切な使用タイミングを理解する

はじめに

これまでは、ユーザーの入力に応じてStateを更新し、画面を変更する方法を学んできました。しかし、実際のWebアプリケーションでは、コンポーネントが表示される際にサーバーからデータを取得したり、一定時間後に処理を実行したりする必要があることがよくあります。

このような「コンポーネントの描画以外の処理」をサイドエフェクトと呼びます。Reactでは、useEffectというフックを使ってサイドエフェクトを安全に管理できます。

本章では、useEffectの基本的な使い方から、実際のAPI通信やタイマー処理まで、段階的に学習していきましょう。

サイドエフェクト(副作用)について

サイドエフェクトとは

まず、サイドエフェクトという概念を理解しましょう。

サイドエフェクトとは、コンポーネントの描画(レンダリング)以外で発生する処理のことです。 「副作用」とも呼ばれることもあります。

わかりやすくいうと、コンポーネントの表示に直接関係しないものの、アプリケーションの動作に影響を与える処理全般がサイドエフェクトです。

例えば、コンポーネントが表示されたときにサーバーからデータを取得したり、タイマーを設定して一定時間後に何かを実行したりすることがサイドエフェクトに該当します。

たとえば以下のような処理がサイドエフェクトに該当します。

  • サーバーからデータを取得する(API通信)
  • タイマーやインターバルの設定
  • ブラウザのタイトルを変更する
  • ローカルストレージにデータを保存する
  • DOM要素を直接操作する

なぜuseEffectが必要なのか

通常のJavaScriptでは、これらの処理を関数の中に直接書くことができます。 しかし、Reactのコンポーネント関数では、以下の理由でサイドエフェクトを直接書くべきではありません。

  • コンポーネントは何度も再実行される可能性がある
  • 予期しないタイミングで処理が実行される可能性がある
  • パフォーマンスの問題が発生する可能性がある

いまいちピンと来ないかもしれませんが、これはReactの裏側の仕組みによるものですので「こういうもの」と覚えてしまっても大丈夫です。

そして useEffectを使うことで、これらの問題を避けて安全にサイドエフェクトを実行できます。

慣れてないうちは、いまいちuseEffectの必要性がピンと来ないかもしれませんが、ひとまずは useEffectを使ってサイドエフェクトを管理することを覚えておきましょう。

useEffectの基本的な使い方

最初に、コンポーネントが表示されたときにメッセージを表示する簡単な例を見てみましょう。 新しいファイルsrc/BasicEffect.jsxを作成し、以下のコードを入力してください。

import { useState, useEffect } from 'react';

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

  // コンポーネントがマウントされた時に実行される
  useEffect(() => {
    console.log('コンポーネントが表示されました');
  }, []); // 空の依存配列

  // countが変更される度に実行される
  useEffect(() => {
    console.log('countが変更されました:', count);
  }, [count]); // countを依存配列に指定

  return (
    <div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-md">
      <h2 className="text-2xl font-bold mb-4 text-center">useEffect の基本</h2>

      <div className="text-center mb-4">
        <p className="text-lg mb-2">カウント: {count}</p>
        <button
          onClick={() => setCount(prev => prev + 1)}
          className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
        >
          +1
        </button>
      </div>

      <div className="text-sm text-gray-600">
        <p>ブラウザの開発者ツールのコンソールを確認してください</p>
      </div>
    </div>
  );
}

export default BasicEffect;

このコードでは、2つのuseEffectを使用しています。

まず、コンポーネントが最初に表示された時にメッセージを表示するためのuseEffectがあります。

useEffect(() => {
  console.log('コンポーネントが表示されました');
}, []); // 空の依存配列

次に、countの値が変更されるたびにメッセージを表示するためのuseEffectがあります。

useEffect(() => {
  console.log('countが変更されました:', count);
}, [count]); // countを依存配列に指定

一見、なぜこれを useEffectで行う必要があるのか疑問に思うかもしれません。 しかし、Reactではコンポーネントが再レンダリングされるたびに処理を実行することは避けるべきとされています。 そのため、useEffectを使って、特定のタイミングでのみ処理を実行するようにしています。

初めはuseEffectの使い方に戸惑うかもしれませんが、慣れてくると非常に便利な機能であることがわかります。

ちなみに、空の依存配列[]を指定しているのか、また[count]を指定しているのかについては、後ほど詳しく説明します。

ではApp.jsxにこのコンポーネントを追加して、表示できるようにします。

import BasicEffect from './BasicEffect';

function App() {
  return (
    <div className="min-h-screen bg-gray-100 py-8">
      <div className="container mx-auto">
        <h1 className="text-3xl font-bold text-center mb-8">useEffect の基本</h1>
        <BasicEffect />
      </div>
    </div>
  );
}
export default App;

開発サーバーを起動して、ブラウザの開発者ツールのコンソールを確認してみましょう。

npm run dev

ブラウザを開いて、コンソールタブを表示してください。 最初にコンポーネントが表示された時に「コンポーネントが表示されました」というメッセージが表示されるはずです。

コンポーネントが表示されました
countが変更されました: 0 // 初期値の0が表示されます

なお、React 18 以降では、コンポーネントの初期表示時に 2回レンダリングされることがありますが、正常な動作です。

これは、開発モードでのパフォーマンス向上のための仕組みで、実際の本番環境では1回だけレンダリングされるようになります。

さてllに戻りますと、ボタンをクリックしてcountの値を増やすと、「countが変更されました: 1」というメッセージが表示されます。

countが変更されました: 1

このように、useEffectを使うことで、コンポーネントのライフサイクルに応じて特定の処理を実行することができます。

依存配列の使い方

先ほどのuseEffectの例では、依存配列を使って処理の実行タイミングを制御しました。

まず一つ目のuseEffectでは、空の依存配列[]を指定しました。

useEffect(() => {
  console.log('コンポーネントが表示されました');
}, []); // 空の依存配列

この場合、コンポーネントが最初にマウントされた時にのみ実行されます。

これは定番のパターンで、コンポーネントが初めて表示された時に一度だけ実行したい処理に使います。

次に、二つ目のuseEffectでは、countを依存配列に指定しました。

useEffect(() => {
  console.log('countが変更されました:', count);
}, [count]); // countを依存配列に指定

この場合、countの値が変更されるたびに実行されます。

今回はcountが更新されるたびにコンソールにメッセージを表示していますが、実際のアプリケーションでは、API通信やDOM操作など、countの値に依存する処理をここに書くことが多いです。

API通信でデータを取得する

次に、より実用的な例として、外部のAPIからデータを取得してみましょう。 JSONPlaceholderという無料のテスト用APIを使って、ユーザー情報を取得します。

新しいファイルsrc/UserData.jsxを作成し、以下のコードを入力してください。

import { useState, useEffect } from 'react';

function UserData() {
  // ユーザーデータを管理するState
  const [user, setUser] = useState(null);
  // ローディング状態を管理するState
  const [loading, setLoading] = useState(true);

  // コンポーネントがマウントされた時にAPIからデータを取得
  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('データの取得に失敗しました:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, []); // 空の依存配列でマウント時のみ実行

  // ローディング中の表示
  if (loading) {
    return (
      <div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-md">
        <div className="text-center">
          <p className="text-lg">データを読み込み中...</p>
        </div>
      </div>
    );
  }

  return (
    <div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-md">
      <h2 className="text-2xl font-bold mb-4 text-center">ユーザー情報</h2>

      {user && (
        <div className="space-y-2">
          <div className="p-3 bg-gray-100 rounded">
            <p className="text-sm text-gray-600">名前</p>
            <p className="font-bold text-lg">{user.name}</p>
          </div>

          <div className="p-3 bg-gray-100 rounded">
            <p className="text-sm text-gray-600">メールアドレス</p>
            <p className="font-bold">{user.email}</p>
          </div>
        </div>
      )}
    </div>
  );
}

export default UserData;

このコードでは、以下のことを行っています。

  1. useStateを使って、ユーザーデータとローディング状態を管理
  2. useEffectを使って、コンポーネントがマウントされた時にAPIからユーザーデータを取得
  3. データ取得中はローディングメッセージを表示
  4. データ取得が完了したら、ユーザーの名前とメールアドレスを表示

それぞれ、部分ごとに詳しく見ていきましょう。

まず、useStateを使ってユーザーデータとローディング状態を管理します。

const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

ここでは、userというStateにAPIから取得したユーザーデータを保存し、loadingというStateでデータの読み込み中かどうかを管理しています。

次に、useEffectを使って、コンポーネントがマウントされた時にAPIからデータを取得します。

useEffect(() => {
  const fetchUser = async () => {
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
      const userData = await response.json();
      setUser(userData);
    } catch (error) {
      console.error('データの取得に失敗しました:', error);
    } finally {
      setLoading(false);
    }
  };

  fetchUser();
}, []); // 空の依存配列でマウント時のみ実行

この部分では、非同期関数fetchUserを定義し、fetchを使ってAPIからユーザーデータを取得しています。

なお、依存配列を空の[]にしているため、このuseEffectはコンポーネントが最初にマウントされた時にのみ実行されます。

fetchはPromiseを返すため、awaitを使って非同期処理を待機しています。

データの取得に成功したら、setUserを使ってユーザーデータをStateに保存します。

もしデータの取得に失敗した場合は、console.errorでエラーメッセージを表示します。

最後に、データの取得が完了したら、ローディング状態をfalseに更新します。

finally {
  setLoading(false);
}

これが無いと、ローディング状態が永遠にtrueのままになり、ユーザーに何も表示されなくなってしまいますので注意が必要です。

ローディング状態を管理することで、データが読み込まれるまでの間、ユーザーに待機中のメッセージを表示できます。

if (loading) {
  return (
    <div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-md">
      <div className="text-center">
        <p className="text-lg">データを読み込み中...</p>
      </div>
    </div>
  );
}

そしてデータの取得が完了したら、ユーザーの名前とメールアドレスを表示します。

return (
  <div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-md">
    <h2 className="text-2xl font-bold mb-4 text-center">ユーザー情報</h2>

    {user && (
      <div className="space-y-2">
        <div className="p-3 bg-gray-100 rounded">
          <p className="text-sm text-gray-600">名前</p>
          <p className="font-bold text-lg">{user.name}</p>
        </div>

        <div className="p-3 bg-gray-100 rounded">
          <p className="text-sm text-gray-600">メールアドレス</p>
          <p className="font-bold">{user.email}</p>
        </div>
      </div>
    )}
  </div>
);

この部分では、userが存在する場合にのみユーザー情報を表示しています。 これにより、データがまだ取得されていない状態でエラーが発生するのを防ぎます。

では、App.jsxにこのコンポーネントを追加して、表示できるようにします。

import UserData from './UserData';
function App() {
  return (
    <div className="min-h-screen bg-gray-100 py-8">
      <div className="container mx-auto">
        <h1 className="text-3xl font-bold text-center mb-8">API通信でデータを取得</h1>
        <UserData />
      </div>
    </div>
  );
}
export default App;

開発サーバーを起動して、ブラウザで確認してみましょう。

npm run dev

ブラウザを開いて、ユーザー情報が正しく表示されることを確認してください。

スクリーンショット

このように、useEffectを使うことで、コンポーネントのライフサイクルに応じてAPI通信を行い、外部データを取得することができます。

基本的には、API のように外部データを利用する場合は useEffectを使うことが一般的です。

タイマー処理とクリーンアップ

最後に、タイマー機能を実装して、useEffectのクリーンアップ機能を学びましょう。 3秒のカウントダウンタイマーを作成します。

新しいファイルsrc/Timer.jsxを作成し、以下のコードを入力してください。

import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(3);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    let interval = null;

    if (isActive && seconds > 0) {
      // 1秒ごとにsecondsを1減らす
      interval = setInterval(() => {
        setSeconds(prev => prev - 1);
      }, 1000);
    }

    // クリーンアップ関数
    return () => {
      if (interval) {
        clearInterval(interval);
      }
    };
  }, [isActive, seconds]); // isActiveとsecondsの変化を監視

  // タイマーを開始する関数
  const startTimer = () => {
    setIsActive(true);
  };

  // タイマーをリセットする関数
  const resetTimer = () => {
    setIsActive(false);
    setSeconds(3);
  };

  return (
    <div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-md">
      <h2 className="text-2xl font-bold mb-4 text-center">3秒タイマー</h2>

      <div className="text-center">
        <div className="text-6xl font-bold mb-6 text-blue-600">
          {seconds}
        </div>

        {seconds === 0 ? (
          <div className="mb-4">
            <p className="text-2xl font-bold text-green-600 mb-4">時間です!</p>
            <button
              onClick={resetTimer}
              className="bg-green-500 text-white px-6 py-2 rounded hover:bg-green-600"
            >
              リセット
            </button>
          </div>
        ) : (
          <div className="space-x-4">
            <button
              onClick={startTimer}
              disabled={isActive}
              className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600 disabled:bg-gray-400"
            >
              {isActive ? 'カウント中...' : 'スタート'}
            </button>
            <button
              onClick={resetTimer}
              className="bg-gray-500 text-white px-6 py-2 rounded hover:bg-gray-600"
            >
              リセット
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

export default Timer;

このコードでは、以下のことを行っています。

  1. useStateを使って、残り時間(秒数)とタイマーのアクティブ状態を管理
  2. useEffectを使って、タイマーの開始とクリーンアップを行う
  3. タイマーを開始する関数とリセットする関数を定義
  4. タイマーの残り時間を表示し、時間が来たらメッセージを表示
  5. スタートボタンとリセットボタンを用意
  6. クリーンアップ関数でタイマーを適切に解除

それぞれ、部分ごとに詳しく見ていきましょう。 まず、useStateを使って、残り時間とタイマーのアクティブ状態を管理します。

const [seconds, setSeconds] = useState(3);
const [isActive, setIsActive] = useState(false);

ここでは、secondsというStateに残り時間を保存し、isActiveというStateでタイマーがアクティブかどうかを管理しています。 次に、useEffectを使って、タイマーの開始とクリーンアップを行います。

useEffect(() => {
  let interval = null;

  if (isActive && seconds > 0) {
    // 1秒ごとにsecondsを1減らす
    interval = setInterval(() => {
      setSeconds(prev => prev - 1);
    }, 1000);
  }

  // クリーンアップ関数
  return () => {
    if (interval) {
      clearInterval(interval);
    }
  };
}, [isActive, seconds]); // isActiveとsecondsの変化を監視

この部分では、isActivetrueで、かつsecondsが0より大きい場合に1秒ごとにsecondsを1減らす処理を行います。 setIntervalを使って1秒ごとにsetSecondsを呼び出し、残り時間を更新しています。

また、useEffectのクリーンアップ関数を使って、コンポーネントがアンマウントされる際や依存値が変化する際にタイマーを適切に解除します。

return () => {
  if (interval) {
    clearInterval(interval);
  }
};

このクリーンアップ関数は、useEffectが再実行される前やコンポーネントがアンマウントされる際に呼び出されます。 これにより、タイマーが不要な状態で残り続けることを防ぎます。

クリーンアップと聞くと難しく感じるかもしれませんが、要は「不要な処理をきちんと片付ける」ことです。 タイマーの処理が不要になった時に、きちんと解除することで、メモリリークや予期しない動作を防ぐことができます。 次に、タイマーを開始する関数とリセットする関数を定義します。

const startTimer = () => {
  setIsActive(true);
};
const resetTimer = () => {
  setIsActive(false);
  setSeconds(3);
};

startTimer関数は、タイマーを開始するためにisActivetrueに設定します。 そして、resetTimer関数はタイマーをリセットして、isActivefalseにし、残り時間を3秒に戻します。 最後に、タイマーの残り時間を表示し、時間が来たらメッセージを表示します。

return (
  <div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-md">
    <h2 className="text-2xl font-bold mb-4 text-center">3秒タイマー</h2>

    <div className="text-center">
      <div className="text-6xl font-bold mb-6 text-blue-600">
        {seconds}
      </div>

      {seconds === 0 ? (
        <div className="mb-4">
          <p className="text-2xl font-bold text-green-600 mb-4">時間です!</p>
          <button
            onClick={resetTimer}
            className="bg-green-500 text-white px-6 py-2 rounded hover:bg-green-600"
          >
            リセット
          </button>
        </div>
      ) : (
        <div className="space-x-4">
          <button
            onClick={startTimer}
            disabled={isActive}
            className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600 disabled:bg-gray-400"
          >
            {isActive ? 'カウント中...' : 'スタート'}
          </button>
          <button
            onClick={resetTimer}
            className="bg-gray-500 text-white px-6 py-2 rounded hover:bg-gray-600"
          >
            リセット
          </button>
        </div>
      )}
    </div>
  </div>
);

この部分では、残り時間を大きなフォントで表示し、時間が来たら「時間です!」というメッセージを表示します。 また、スタートボタンとリセットボタンを用意し、スタートボタンはタイマーがアクティブな場合は無効化しています。 では、App.jsxにこのコンポーネントを追加して、表示できるようにします。

import Timer from './Timer';

function App() {
  return (
    <div className="min-h-screen bg-gray-100 py-8">
      <div className="container mx-auto">
        <h1 className="text-3xl font-bold text-center mb-8">タイマー機能</h1>
        <Timer />
      </div>
    </div>
  );
}
export default App;

開発サーバーを起動して、ブラウザで確認してみましょう。

npm run dev

ブラウザを開いて、3秒のカウントダウンタイマーが正しく動作することを確認してください。 スクリーンショット タイマーがスタートボタンをクリックするとカウントダウンが始まり、3秒後に「時間です!」というメッセージが表示されます。

スクリーンショット

スクリーンショット

React アプリでは、よくタイマー処理を行うことがありますが、useEffectを使うことで、タイマーの開始とクリーンアップを適切に管理できます。

まとめ

本章では、useEffectを使ったサイドエフェクトの管理について学習しました。 以下のポイントを理解できたことと思います。

  • サイドエフェクトとは、コンポーネントの描画以外で発生する処理のこと
  • useEffectを使ってサイドエフェクトを安全に管理できる
  • 依存配列によって、処理が実行されるタイミングを制御できる
  • API通信やタイマー処理など、実用的な機能を実装できる
  • クリーンアップ関数でリソースの適切な解放ができる
  • 無限ループや不要な再実行を避ける書き方が重要

useEffectは、動的で魅力的なWebアプリケーションを作成するために欠かせないReactの機能です。

最初のうちは戸惑うこともあるかもしれませんが、使いこなせるようになると非常に強力なツールとなります。

API通信やタイマー処理などの基本を理解することで、より高度な機能を持つアプリケーションの開発に進むことができます。

このセクションは有料サブスクリプションへの登録、またはログインが必要です。完全なコンテンツにアクセスするには、料金ページ(/pricing)をご覧ください。購入済みの場合は、ログインしてください。

Basicプランでより詳しく学習

この先のコンテンツを読むにはBasicプラン以上が必要です。より詳細な解説、実践的なサンプルコード、演習問題にアクセスして学習を深めましょう。

作成者:とまだ
Previous
実践的なStateの活用