教材
ShinCodeさんの
【完全保存版】React Hooksを完全に理解するHooksマスター講座【React18~19対応】
を元に、私が学んだ内容をまとめていきますわ。
お嬢様的Hooks解説
UseEffectとは
useEffect
は、Reactのフックの一つで、副作用(side effects)を管理するためのものですわ。
副作用(Effect)とは
副作用とは、コンポーネントのレンダリングに直接関係しないが、何かしらの処理が必要な操作のことですの。
イベント(Event)との対比
-
イベント(Event): ボタンを押してカウントアップするなど、ユーザーの操作によって発生するもの。例えば、ボタンのクリックやフォームの送信などに対応する関数を実行することですわ。
- 副作用(Effect): Twitterのタイムラインのように、何もしていなくても他の人の投稿がリアルタイムで反映されるような操作のことですわ。イベントに対して外部システムの変更が発生した時に行われる処理ですわ。データの取得やAPIの呼び出し、初回リロード時の設定などが該当しますの。
useEffectの用途
-
データの取得: APIを呼び出してデータを取得する際に使いますの。
- 初期化処理: コンポーネントの初回レンダリング時に一度だけ実行したい処理を記述しますの。
ブラウザイベントのリッスン
カーソルが動くことにより円が動く機能(ポインタームーブ)を実装するためには、ウィンドウオブジェクトを利用する必要がございますわ。
ウィンドウオブジェクトに関する詳細は、こちらをご参照くださいませ。
Reactではウィンドウオブジェクトの操作をJSX
内で直接行うことができませんの。そこで、useEffect
フックを使用しますの。
useEffect
useEffect
フックは、副作用を管理するためのもので、外部システム(APIなど)に対して何か作用を起こしたいときに使用しますの。依存配列を空にすると、コンポーネントの初回レンダリング時に一度だけ実行されますわ。
ポインタームーブの実装例
以下に、マウスの位置に応じて円が動く機能を実装した例を示しますわ。
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;
-
useState:
position
という状態をuseState
で管理し、初期値を{ x: 0, y: 0 }
に設定しますの。
-
useEffect:
第一引数にアロー関数、第二引数に依存配列を渡しますの。この例では依存配列が空のため、コンポーネントの初回レンダリング時に一度だけ実行されますの。handleMove
関数を定義し、ポインターが動くたびにsetPosition
を呼び出して位置を更新しますの。window.addEventListener
でポインタームーブイベントをリッスンし、handleMove
関数を実行しますの。クリーンアップ関数を返すことで、コンポーネントがアンマウントされるときにイベントリスナーを削除しますの。
クリーンアップ関数
useEffect
フックを使用する際には、イベントリスナーの追加だけでなく、クリーンアップ関数を使ってイベントの監視をやめることが重要ですわ。これにより、ページを閉じた時やコンポーネントがアンマウントされる時にリソースの無駄を防ぎますの。
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秒間待って返しますの。
// 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;
}
初期実装
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アクセスなどの非同期的な処理を行うために、async
、await
を使用しますわ。ただし、useEffect
自体は非同期関数を直接受け取れないため、別にstartFetching
のような関数を内部に作成し、それに対してasync
をつけますわ。
依存配列を指定してEffect発火条件を変更しよう
useEffect
の第二引数に依存配列を指定することで、発火条件を変更できますの。例えば、person
が変化するタイミングでuseEffect
を発火させますわ。
const [person, setPerson] = useState<string>("OjyoSama")
useEffect(() => {
const startFetching = async () => {
const response = await fetchBio(person);
console.log(response);
};
startFetching();
}, [person]);
誰を選択しているかpタグで表示させる
以下のコードでは、選択された人物のbio情報を表示するために、useState
でbio
の初期値をnull
にし、fetchBio
関数の結果をsetBio
で更新しますわ。p
タグで表示する際に、bio
がnull
の時には"Loading..."と表示しますの。
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;
説明
useState
でbio
の初期値をnull
に設定: 非同期処理が完了するまでの間、"Loading..."と表示されますわ。
依存配列[person]
: person
が変わるたびにuseEffect
が発火し、新しいbio情報を取得しますの。
bio ?? "Loading..."
: Null 合体演算子(??)を使って、bio
がnull
またはundefined
の場合に"Loading..."を表示しますの。詳しくはこちらをご覧くださいませ。
クリーンアップ関数で競合状態を解決しよう
ユーザーが連続でボタンを変えたりすると、意図しない挙動が起こったりバグの原因になることがございますわ。これを防ぐために、ignore
フラグを用意して、切り替え前にクリーンアップを行うことができますの。
競合状態の解決例
以下のコードでは、ignore
フラグを使用して競合状態を解決していますわ。
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]);
説明
-
useEffect
フックが実行されるたびに、ignore
フラグをfalse
に設定しますの。
- 非同期関数
startFetching
を定義し、fetchBio
を呼び出してレスポンスを取得しますの。
- レスポンスを取得した後、
ignore
フラグがfalse
であれば、setBio
を呼び出してbio
の状態を更新しますの。
-
useEffect
フックがクリーンアップされる時(依存配列[person]
が更新される直前やコンポーネントがアンマウントされる時)に、ignore
フラグをtrue
に設定しますの。
これにより、データの取得が完了する前にコンポーネントがアンマウントされた場合でもsetBio
が呼ばれないようになりますわ。
無限ループ
依存配列にset
関数の結果を含めると無限ループが発生する可能性がありますの。例えば、以下のようなコードですわ。
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
が長すぎて可読性が低くなっておりますわ。
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
の部分を別のファイルに切り出して、カスタムフックを作成しますわ。
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
を使うことで、コンポーネントのコードがシンプルになりますの。
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
を使用してユーザー情報を取得するコンポーネントの実装例を示しますわ。
// 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.addEventListener
とwindow.removeEventListener
をuseEffect
内で適切に使用する例が紹介されておりますわ。
非同期データフェッチングと競合状態の解決
APIからデータを非同期に取得する場合、クリーンアップ関数を使用してコンポーネントのアンマウント時に競合状態を防ぐことが大切ですわ。
カスタムフックの活用
複雑なロジックを再利用可能な形にするために、カスタムフックを作成することで、コードの可読性とメンテナンス性を向上させますの。
useSWRによるキャッシュデータフェッチング
useSWR
フックを使用することで、キャッシュされたデータを効率的に取得し、パフォーマンスを向上出来ますわ。useSWR
は自動的にキャッシュを管理し、同じデータの再リクエストを防ぎますので、コードが簡潔になり、手動でのキャッシュ管理が不要になりますの。