7
6

お嬢様が教えるUseEffect()

Last updated at Posted at 2024-06-05

教材

ShinCodeさんの

完全保存版】React Hooksを完全に理解するHooksマスター講座【React18~19対応】

を元に、私が学んだ内容をまとめていきますわ。

お嬢様的Hooks解説

お嬢様が解説するuseState()

お嬢様が解説するuseEffect()

お嬢様が解説するuseRef()

UseEffectとは

useEffectは、Reactのフックの一つで、副作用(side effects)を管理するためのものですわ。

副作用(Effect)とは

副作用とは、コンポーネントのレンダリングに直接関係しないが、何かしらの処理が必要な操作のことですの。

イベント(Event)との対比

  • イベント(Event): ボタンを押してカウントアップするなど、ユーザーの操作によって発生するもの。例えば、ボタンのクリックやフォームの送信などに対応する関数を実行することですわ。

  • 副作用(Effect): Twitterのタイムラインのように、何もしていなくても他の人の投稿がリアルタイムで反映されるような操作のことですわ。イベントに対して外部システムの変更が発生した時に行われる処理ですわ。データの取得やAPIの呼び出し、初回リロード時の設定などが該当しますの。

useEffectの用途

  • データの取得: APIを呼び出してデータを取得する際に使いますの。

  • 初期化処理: コンポーネントの初回レンダリング時に一度だけ実行したい処理を記述しますの。

ブラウザイベントのリッスン

カーソルが動くことにより円が動く機能(ポインタームーブ)を実装するためには、ウィンドウオブジェクトを利用する必要がございますわ。

ウィンドウオブジェクトに関する詳細は、こちらをご参照くださいませ。

Reactではウィンドウオブジェクトの操作をJSX内で直接行うことができませんの。そこで、useEffectフックを使用しますの。

useEffect

useEffectフックは、副作用を管理するためのもので、外部システム(APIなど)に対して何か作用を起こしたいときに使用しますの。依存配列を空にすると、コンポーネントの初回レンダリング時に一度だけ実行されますわ。

ポインタームーブの実装例

以下に、マウスの位置に応じて円が動く機能を実装した例を示しますわ。

Lesson2_1.tsx
import { useEffect, useState } from "react";

const Lesson2_1 = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener("pointermove", handleMove);
    
    return () => {
      window.removeEventListener("pointermove", handleMove);
    };
  }, []);

  return (
    <div
      style={{
        position: "absolute",
        backgroundColor: "blue",
        borderRadius: "50%",
        opacity: 0.6,
        pointerEvents: "none",
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -20,
        top: -20,
        width: 50,
        height: 50,
      }}
    ></div>
  );
}

export default Lesson2_1;

  1. useState:
    positionという状態をuseStateで管理し、初期値を{ x: 0, y: 0 }に設定しますの。

  2. useEffect:
    第一引数にアロー関数、第二引数に依存配列を渡しますの。この例では依存配列が空のため、コンポーネントの初回レンダリング時に一度だけ実行されますの。

    handleMove関数を定義し、ポインターが動くたびにsetPositionを呼び出して位置を更新しますの。

    window.addEventListenerでポインタームーブイベントをリッスンし、handleMove関数を実行しますの。

    クリーンアップ関数を返すことで、コンポーネントがアンマウントされるときにイベントリスナーを削除しますの。

スクリーンショット 2024-06-04 10.52.41.png

クリーンアップ関数

useEffectフックを使用する際には、イベントリスナーの追加だけでなく、クリーンアップ関数を使ってイベントの監視をやめることが重要ですわ。これにより、ページを閉じた時やコンポーネントがアンマウントされる時にリソースの無駄を防ぎますの。

Lesson2_1.tsx
const Lesson2_1 = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener("pointermove", handleMove);

    // クリーンアップ関数を追加
    return () => {
      window.removeEventListener("pointermove", handleMove);
    }
  }, []);

このコードにより、マウスの位置を監視しながら、コンポーネントのアンマウント時にイベントリスナーを解除してリソースの無駄を防ぐことができますの。

データフェッチング

useEffectを使ってAPIアクセスなどの非同期的な処理を行う際には、クリーンアップ関数と同様に非同期関数を適切に扱うことが重要ですわ。以下に、データフェッチングの例を示しますわ。

データフェッチング関数

まず、fetchBioという関数を定義しますの。これは、人物の名前を引数として受け取り、その人物のbioを1秒間待って返しますの。

fetchBio.ts
// personの名前を渡すと1秒間待機して文字列を返す。
export async function fetchBio(person: string) {
  // 仮のネットワークレイテンシをシミュレートするために、少し遅延させます。
  await new Promise((resolve) => setTimeout(resolve, 1000));

  const bio = `This is a ${person}'s bio`;
  return bio;
}

初期実装

Lesson2_2.tsx
import { useEffect, useState } from "react";
import { fetchBio } from "./fetchBio";

const Lesson2_2 = () => {
  const [person, setPerson] = useState<string>("OjyoSama")
  useEffect(() => {
    const startFetching = async () => {
      const response = await fetchBio(person);
      console.log(response);
    };
    startFetching();
  }, [])
  return (
    <div>
      <select onChange={(e) => setPerson(e.target.value)} value={person}>
        <option value="OjyoSama">OjyoSama</option>
        <option value="TestUser">TestUser</option>
        <option value="SampleUser">SampleUser</option>
      </select>

      <hr />

      <p className="text-black">{"Loading..."}</p>
    </div>
  );
};

export default Lesson2_2;

非同期処理

ReactでAPIアクセスなどの非同期的な処理を行うために、asyncawaitを使用しますわ。ただし、useEffect自体は非同期関数を直接受け取れないため、別にstartFetchingのような関数を内部に作成し、それに対してasyncをつけますわ。

async functionについての詳細はこちら

依存配列を指定してEffect発火条件を変更しよう

useEffectの第二引数に依存配列を指定することで、発火条件を変更できますの。例えば、personが変化するタイミングでuseEffectを発火させますわ。

Lesson2_2.tsx
  const [person, setPerson] = useState<string>("OjyoSama")
  useEffect(() => {
    const startFetching = async () => {
      const response = await fetchBio(person);
      console.log(response);
    };
    startFetching();
  }, [person]);

誰を選択しているかpタグで表示させる

以下のコードでは、選択された人物のbio情報を表示するために、useStatebioの初期値をnullにし、fetchBio関数の結果をsetBioで更新しますわ。pタグで表示する際に、bionullの時には"Loading..."と表示しますの。

Lesson2_2.tsx
import { useEffect, useState } from "react";
import { fetchBio } from "./fetchBio";

const Lesson2_2 = () => {
  const [person, setPerson] = useState<string>("OjyoSama");
  const [bio, setBio] = useState<string | null>(null);

  useEffect(() => {
    const startFetching = async () => {
      const response = await fetchBio(person);
      setBio(response);
    };
    startFetching();
  }, [person]);
  return (
    <div>
      <select onChange={(e) => setPerson(e.target.value)} value={person}>
        <option value="OjyoSama">OjyoSama</option>
        <option value="TestUser">TestUser</option>
        <option value="SampleUser">SampleUser</option>
      </select>

      <hr />

      <p className="text-black">{bio ?? "Loading..."}</p>
    </div>
  );
};

export default Lesson2_2;

説明

useStatebioの初期値をnullに設定: 非同期処理が完了するまでの間、"Loading..."と表示されますわ。

依存配列[person]: personが変わるたびにuseEffectが発火し、新しいbio情報を取得しますの。

bio ?? "Loading...": Null 合体演算子(??)を使って、bionullまたはundefinedの場合に"Loading..."を表示しますの。詳しくはこちらをご覧くださいませ。

クリーンアップ関数で競合状態を解決しよう

ユーザーが連続でボタンを変えたりすると、意図しない挙動が起こったりバグの原因になることがございますわ。これを防ぐために、ignoreフラグを用意して、切り替え前にクリーンアップを行うことができますの。

競合状態の解決例

以下のコードでは、ignoreフラグを使用して競合状態を解決していますわ。

Lesson2_2.tsx
  const [person, setPerson] = useState<string>("OjyoSama");
  const [bio, setBio] = useState<string | null>(null);

  useEffect(() => {
    let ignore = false;
    const startFetching = async () => {
      const response = await fetchBio(person);
      if (!ignore) {
        setBio(response);
      }
    };
    startFetching();

    // 依存配列が更新される直前に、このアロー関数が呼ばれる。
    return () => {
      ignore = true;
    }
  }, [person]);

説明

  1. useEffectフックが実行されるたびに、ignoreフラグをfalseに設定しますの。

  2. 非同期関数startFetchingを定義し、fetchBioを呼び出してレスポンスを取得しますの。

  3. レスポンスを取得した後、ignoreフラグがfalseであれば、setBioを呼び出してbioの状態を更新しますの。

  4. useEffectフックがクリーンアップされる時(依存配列[person]が更新される直前やコンポーネントがアンマウントされる時)に、ignoreフラグをtrueに設定しますの。

これにより、データの取得が完了する前にコンポーネントがアンマウントされた場合でもsetBioが呼ばれないようになりますわ。

スクリーンショット 2024-06-04 11.31.51.png

無限ループ

依存配列にset関数の結果を含めると無限ループが発生する可能性がありますの。例えば、以下のようなコードですわ。

Lesson2_2.tsx
const [person, setPerson] = useState < string > "OjyoSama";
const [bio, setBio] = (useState < string) | (null > null);
const [count, setCount] = useState(0);

useEffect(() => {
  let ignore = false;
  const startFetching = async () => {
    const response = await fetchBio(person);
    if (!ignore) {
      setBio(response);
    }
  };
  startFetching();

  setCount(count + 1);

  // 依存配列が更新される直前に、このアロー関数が呼ばれる。
  return () => {
    ignore = true;
  };
}, [person, count]);

問題点

この場合、依存配列にcountを含めると、setCountによってcountが更新されるたびにuseEffectが再実行されますわ。これが無限ループを引き起こしますの。

カスタムフックス

Reactでロジックが似通った部分を再利用可能にするためには、カスタムフックスを作成して使い回すのが有効ですわ。カスタムフックスを使うことで、コードの可読性とメンテナンス性を向上させることができますの。

カスタムフックの作成例

以下に、ユーザー情報を取得するためのカスタムフックuseFetchUserを作成する例を示しますわ。

元のコード

まず、以下のコードがあったとしますの。このコードでは、useEffectが長すぎて可読性が低くなっておりますわ。

Lesson2_3.tsx
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    city: string;
  };
}

const Lesson2_3 = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState<boolean>(true);

  useEffect(() => {
    let isMounted = true; // このフラグはコンポーネントのマウント状態を追跡します

    const fetchUser = async () => {
      try {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/users/1"
        );
        if (!response.ok) {
          throw new Error("データの取得に失敗しました");
        }
        const userData: User = await response.json();

        if (isMounted) {
          setUser(userData);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) {
          console.error(error);
          setLoading(false);
        }
      }
    };

    fetchUser();

    // クリーンアップ関数
    return () => {
      isMounted = false; // コンポーネントがアンマウントされたらフラグをfalseに設定
    };
  }, []); // 空の依存配列

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

  if (!user) {
    return <div>ユーザー情報が見つかりません。</div>;
  }

  return (
    <div>
      <h1>ユーザー情報</h1>
      <p>
        <strong>名前:</strong> {user.name}
      </p>
      <p>
        <strong>ユーザー名:</strong> {user.username}
      </p>
      <p>
        <strong>Email:</strong> {user.email}
      </p>
      <p>
        <strong>都市:</strong> {user.address.city}
      </p>
    </div>
  );
};

export default Lesson2_3;

カスタムフックuseFetchUserの作成

useEffectの部分を別のファイルに切り出して、カスタムフックを作成しますわ。

useFetchUser.ts
import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    city: string;
  };
}

export const useFetchUser = (userId: number) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState<boolean>(true);

  useEffect(() => {
    let isMounted = true; // このフラグはコンポーネントのマウント状態を追跡します

    const fetchUser = async () => {
      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        );
        if (!response.ok) {
          throw new Error("データの取得に失敗しました");
        }
        const userData: User = await response.json();

        if (isMounted) {
          setUser(userData);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) {
          console.error(error);
          setLoading(false);
        }
      }
    };

    fetchUser();

    // クリーンアップ関数
    return () => {
      isMounted = false; // コンポーネントがアンマウントされたらフラグをfalseに設定
    };
  }, [userId]); // 空の依存配列

  return {user, loading}
}

カスタムフックの使用

カスタムフックuseFetchUserを使うことで、コンポーネントのコードがシンプルになりますの。

Lesson2_3.tsx
import { useFetchUser } from "./hooks/useFetchUser";

const Lesson2_3 = () => {
  
  //カスタムフックス
  const {user, loading} = useFetchUser(0);

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

  if (!user) {
    return <div>ユーザー情報が見つかりません。</div>;
  }

  return (
    <div>
      <h1>ユーザー情報</h1>
      <p>
        <strong>名前:</strong> {user.name}
      </p>
      <p>
        <strong>ユーザー名:</strong> {user.username}
      </p>
      <p>
        <strong>Email:</strong> {user.email}
      </p>
      <p>
        <strong>都市:</strong> {user.address.city}
      </p>
    </div>
  );
};

export default Lesson2_3;

useSWR()を使ったキャッシュデータフェッチング

useSWRは、Vercelが提供するReact Hooksライブラリで、キャッシュされたデータフェッチングを行うことができますの。高速かつ軽量で再利用可能なデータフェッチングを実現するために使われますの。

セットアップ

まず、swrライブラリをインストールしますの。

npm i swr

親コンポーネントの実装

以下に、useSWRを使用してユーザー情報を取得するコンポーネントの実装例を示しますわ。

Lesson2_3.tsx
// import { useFetchUser } from "./hooks/useFetchUser";

import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then(r => r.json())

const Lesson2_3 = () => {
  
  //カスタムフックス
  const {data: user, isLoading: loading} = useSWR("https://jsonplaceholder.typicode.com/users/1", fetcher)

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

  if (!user) {
    return <div>ユーザー情報が見つかりません。</div>;
  }

  return (
    <div>
      <h1>ユーザー情報</h1>
      <p>
        <strong>名前:</strong> {user.name}
      </p>
      <p>
        <strong>ユーザー名:</strong> {user.username}
      </p>
      <p>
        <strong>Email:</strong> {user.email}
      </p>
      <p>
        <strong>都市:</strong> {user.address.city}
      </p>
    </div>
  );
};

export default Lesson2_3;

説明

  • fetcher関数:
    • fetcher関数は、与えられたURLをフェッチしてレスポンスをJSONに変換しますの。

  • useSWR:
    • useSWRはデータを取得し、キャッシュを管理するためのフックですの。useSWRはキャッシュを自動的に管理し、同じデータが再度リクエストされる場合にはキャッシュから返すため、パフォーマンスが向上しますの。

  • 依存関係の管理:
    • useEffectを使用する必要がないため、コードが簡潔になり、読みやすくなりますの。

カスタムフックとuseSWRの比較

useSWRを使用することで、以下のようにコードが簡潔になり、可読性が向上しますの。また、キャッシュの管理も自動化されるため、手動でのキャッシュ管理の必要がなくなりますわ。

まとめ

副作用管理とuseEffectフックの用途

useEffectフックは、コンポーネントのレンダリングに直接関係しない処理(副作用)を管理するために使用しますの。具体的には、APIの呼び出しや初期化処理など、外部システムとの相互作用が必要な場合に使いますの。また、依存配列を指定することで、特定の条件下でのみ実行されるように制御できますの。

イベントリスナーとクリーンアップ関数の重要性

副作用としてイベントリスナーを追加する際には、コンポーネントのアンマウント時にリスナーを削除するクリーンアップ関数を使うことで、リソースの無駄遣いやバグを防ぎますの。例えば、マウスの動きを追跡する機能を実装する際に、window.addEventListenerwindow.removeEventListeneruseEffect内で適切に使用する例が紹介されておりますわ。

非同期データフェッチングと競合状態の解決

APIからデータを非同期に取得する場合、クリーンアップ関数を使用してコンポーネントのアンマウント時に競合状態を防ぐことが大切ですわ。

カスタムフックの活用

複雑なロジックを再利用可能な形にするために、カスタムフックを作成することで、コードの可読性とメンテナンス性を向上させますの。

useSWRによるキャッシュデータフェッチング

useSWRフックを使用することで、キャッシュされたデータを効率的に取得し、パフォーマンスを向上出来ますわ。useSWRは自動的にキャッシュを管理し、同じデータの再リクエストを防ぎますので、コードが簡潔になり、手動でのキャッシュ管理が不要になりますの。

7
6
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
7
6