useEffect
APIからのデータ取得やタイマーなど、コンポーネントの外の世界と連携する「副作用」をuseEffectで管理する方法を学びます。
このチャプターで学ぶこと
- 副作用(side effect)とは何かを説明できる
- useEffectの依存配列の役割と3つのパターンを理解できる
- クリーンアップ関数を使ってメモリリークを防げる
- APIからデータを取得してStateに保存できる
- ローディング状態とエラー状態を適切に管理できる
- JSONPlaceholderからユーザー一覧を取得して表示できる
useEffect
これまで学んだ State とイベントハンドリングで、かなりインタラクティブなUIが作れるようになりました。でも実際のアプリには欠かせない「APIからのデータ取得」がまだです。このチャプターでは useEffect を使って外の世界と連携する方法を学びます。
副作用(Side Effect)とは
React のコンポーネントは本来、props と state を受け取り、JSX を返すだけの「純粋な関数」であるべきです。
しかし実際には、コンポーネントがこんなことをしたい場合があります:
- APIからデータを取得する
- タイマーを設定する
- ブラウザのタイトルを変更する
- LocalStorage にデータを保存する
- WebSocket に接続する
これらはReactのレンダリングサイクルの「外側」にある処理で、副作用(Side Effect) と呼ばれます。useEffect はこれらの副作用を管理するためのフックです。
Laravelのコントローラーで例えると、レスポンスを返す処理(return view(...))が「純粋な処理」で、ログへの書き込み・キャッシュの更新・メール送信などが「副作用」のイメージに近いです。
副作用はメインの処理の「外側」で起きる、付随的な処理です。
useEffect の基本構文
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// ここに副作用の処理を書く
console.log('コンポーネントがレンダリングされた');
});
return <div>...</div>;
}
useEffect は2つの引数を取ります:
- 実行したい関数(副作用の処理)
- 依存配列(いつ実行するかを制御する配列)
依存配列の3パターン
依存配列の書き方によって、useEffect がいつ実行されるかが変わります。
パターン1: 空の配列 [] → マウント時に1回だけ
useEffect(() => {
// コンポーネントが初めて表示されたとき(マウント時)に1回だけ実行
console.log('マウントされた');
fetchData(); // APIからのデータ取得など
}, []); // ← 空の配列
使いどき: 初回データ取得、イベントリスナーの登録、タイマーの開始など
パターン2: 値を指定 [dep] → 値が変わるたびに実行
const [userId, setUserId] = useState(1);
useEffect(() => {
// userId が変わるたびに実行される
fetchUser(userId);
}, [userId]); // ← userId を監視
使いどき: IDが変わったときにデータを再取得する、検索ワードが変わったら検索し直すなど
パターン3: 依存配列なし → 毎レンダリング後に実行
useEffect(() => {
// レンダリングのたびに実行(ほとんど使わない)
document.title = `カウント: ${count}`;
}); // ← 配列なし
依存配列を省略すると、毎回のレンダリング後に実行されます。APIの呼び出しなどを書くと無限ループになる可能性があるので注意が必要です。依存配列は基本的に必ず書きましょう。
3パターンのまとめ
// マウント時に1回だけ
useEffect(() => { ... }, []);
// userId が変わるたびに
useEffect(() => { ... }, [userId]);
// 毎レンダリング後(ほぼ使わない)
useEffect(() => { ... });
クリーンアップ関数
useEffect ではタイマーやイベントリスナーを設定することがあります。コンポーネントが画面から消えるとき(アンマウント時)に、これらを**片付ける(クリーンアップする)**必要があります。
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// タイマーを設定
const intervalId = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// クリーンアップ関数を返す
// コンポーネントがアンマウントされるときに実行される
return () => {
clearInterval(intervalId); // タイマーを止める
console.log('タイマーをクリーンアップした');
};
}, []); // マウント時に1回だけ設定
return <p>経過時間: {seconds}秒</p>;
}
クリーンアップ関数を返すことで、コンポーネントが消えたあともタイマーが動き続けるという問題(メモリリーク)を防げます。
setInterval/setTimeout→clearInterval/clearTimeoutaddEventListener→removeEventListener- WebSocket 接続 → 切断処理
- 外部ライブラリの初期化 → 破棄処理
クリーンアップを怠ると、コンポーネントが画面から消えたあとも処理が走り続け、メモリリークやバグの原因になります。
APIからデータを取得する
いよいよ実践的な例です。useEffect を使ってAPIからデータを取得し、State に保存して表示する方法を学びましょう。
テスト用のAPIとして JSONPlaceholder(https://jsonplaceholder.typicode.com)を使います。無料で使えるフェイクAPIで、ユーザー、投稿、コメントなどのダミーデータを返してくれます。
import { useState, useEffect } from 'react';
function UserList() {
// データを保存するState
const [users, setUsers] = useState([]);
// ローディング状態
const [isLoading, setIsLoading] = useState(true);
// エラー状態
const [error, setError] = useState(null);
useEffect(() => {
// 非同期関数を定義してすぐ呼ぶ
async function fetchUsers() {
try {
setIsLoading(true);
setError(null);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// レスポンスのステータスを確認
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
}
fetchUsers();
}, []); // マウント時に1回だけ取得
// ローディング中
if (isLoading) {
return <p>読み込み中...</p>;
}
// エラー発生時
if (error) {
return <p>エラーが発生しました: {error}</p>;
}
// データ表示
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<strong>{user.name}</strong>
<br />
<small>{user.email}</small>
</li>
))}
</ul>
);
}
useEffect のコールバック関数を直接 async にすることはできません。
// NG: useEffect のコールバックを直接 async にしない
useEffect(async () => {
const data = await fetchData(); // 動くけど推奨されない
}, []);
// OK: 内部に async 関数を作ってすぐ呼ぶ
useEffect(() => {
async function fetchData() {
const data = await fetch(...);
// ...
}
fetchData(); // すぐ呼ぶ
}, []);理由は、useEffect のコールバックはクリーンアップ関数(同期関数)か undefined を返す必要があるためです。async 関数は常に Promise を返すので、型が合いません。
ローディングと エラー状態を管理する理由
Laravelでビューを返すときは、コントローラーでデータを用意してからビューをレンダリングするので「データがない状態」はほぼ発生しません。
でもReactではコンポーネントがレンダリングされた後にデータを取得するため:
- 最初はデータなし(空の配列や null)で表示される
- データ取得中はローディングを表示
- 取得成功したらデータを表示
- 取得失敗したらエラーを表示
この3〜4つの状態を管理するのが基本パターンです。
より実用的な例:IDを変えて再取得する
ユーザーを選択したら、そのユーザーの詳細を取得する例です。
import { useState, useEffect } from 'react';
function UserDetail() {
const [selectedUserId, setSelectedUserId] = useState(1);
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// selectedUserId が変わるたびに実行される
useEffect(() => {
async function fetchUser() {
setIsLoading(true);
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${selectedUserId}`
);
const data = await res.json();
setUser(data);
} catch (err) {
console.error('取得失敗:', err);
} finally {
setIsLoading(false);
}
}
fetchUser();
}, [selectedUserId]); // selectedUserId を依存配列に入れる
return (
<div>
{/* ユーザーIDを切り替えるボタン */}
<div>
{[1, 2, 3, 4, 5].map((id) => (
<button
key={id}
onClick={() => setSelectedUserId(id)}
style={{ fontWeight: selectedUserId === id ? 'bold' : 'normal' }}
>
ユーザー {id}
</button>
))}
</div>
{/* ユーザー詳細 */}
{isLoading ? (
<p>読み込み中...</p>
) : user ? (
<div>
<h2>{user.name}</h2>
<p>メール: {user.email}</p>
<p>電話: {user.phone}</p>
<p>会社: {user.company?.name}</p>
</div>
) : null}
</div>
);
}
依存配列には「useEffect の中で使っている、変化しうる値」を入れます。
// selectedUserId を useEffect の中で使っているので依存配列に入れる
useEffect(() => {
fetchUser(selectedUserId); // selectedUserId を使っている
}, [selectedUserId]); // だから依存配列に入れる依存配列に入れるのを忘れると、古い値を使い続けてバグになります。ESLintの react-hooks/exhaustive-deps ルールを有効にすると、自動で警告してくれます。
ブラウザのタイトルを更新する例
副作用のシンプルな例として、ページタイトルを変更するものも見ておきましょう。
import { useState, useEffect } from 'react';
function PageWithTitle() {
const [count, setCount] = useState(0);
// count が変わるたびにタイトルを更新
useEffect(() => {
document.title = `カウント: ${count} - My App`;
// クリーンアップ: コンポーネントが消えたらタイトルをリセット
return () => {
document.title = 'My App';
};
}, [count]);
return (
<div>
<p>ブラウザのタブタイトルが変わるよ: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
</div>
);
}
演習: JSONPlaceholderからユーザー一覧を取得しよう
https://jsonplaceholder.typicode.com/users から10人のユーザーを取得して、カードスタイルで表示するコンポーネントを作りましょう。
要件:
- ローディング中は「読み込み中…」を表示する
- エラー時は「データの取得に失敗しました」と表示する
- 各ユーザーカードには名前・メールアドレス・電話番号・会社名を表示する
- ユーザー名の頭文字をアバターとして表示する(例:「田中太郎」→「田」)
ユーザーデータの構造:
// JSONPlaceholder が返すユーザーデータの形
{
id: 1,
name: "Leanne Graham",
username: "Bret",
email: "Sincere@april.biz",
phone: "1-770-736-8031 x56442",
company: {
name: "Romaguera-Crona",
// ...
},
// ...
}スターターコード:
import { useState, useEffect } from 'react';
function UserCard({ user }) {
// 名前の最初の1文字をアバターとして使う
const initial = user.name.charAt(0);
return (
<div style={{
border: '1px solid #e2e8f0',
borderRadius: '8px',
padding: '16px',
marginBottom: '12px',
display: 'flex',
alignItems: 'center',
gap: '16px',
}}>
{/* アバター */}
<div style={{
width: '48px',
height: '48px',
borderRadius: '50%',
backgroundColor: '#4f46e5',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
fontWeight: 'bold',
flexShrink: 0,
}}>
{initial}
</div>
{/* ユーザー情報 */}
<div>
<h3 style={{ margin: 0 }}>{user.name}</h3>
<p style={{ margin: '4px 0', color: '#6b7280' }}>{user.email}</p>
<p style={{ margin: '4px 0', color: '#6b7280' }}>{user.phone}</p>
<p style={{ margin: 0, fontSize: '14px' }}>
会社: {user.company.name}
</p>
</div>
</div>
);
}
function UserList() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
// ここに実装しよう
}
fetchUsers();
}, []);
// ローディング・エラー・データ表示のハンドリングを追加しよう
return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<h1>ユーザー一覧</h1>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
export default UserList;チャレンジ:
- 検索ボックスを追加して、名前でリアルタイムフィルタリングできるようにしよう(
useEffectは不要、filter()だけでOK) - 「再読み込み」ボタンを追加しよう(ヒント: 再取得をトリガーするためのStateを使う)
- PokeAPI(
https://pokeapi.co/api/v2/pokemon?limit=20)に切り替えて、ポケモン一覧を表示してみよう
useEffect のよくあるミス
ミス1: 依存配列を忘れて無限ループ
// NG: 無限ループになる!
function BadComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(result => {
setData(result); // State を更新
});
// 依存配列がないと毎レンダリング後に実行 → State更新 → レンダリング → ...
}); // ← 依存配列がない!
return <div>{data}</div>;
}
// OK: [] を付けてマウント時だけ実行
useEffect(() => {
fetchData().then(result => {
setData(result);
});
}, []); // ← 空の配列
ミス2: クリーンアップを忘れる
// NG: コンポーネントが消えてもタイマーが動き続ける
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// クリーンアップなし!
}, []);
// OK: クリーンアップ関数を返す
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id); // ← 忘れずに
}, []);
まとめ
useEffectはレンダリングの後に副作用を実行するフック- 依存配列
[]: マウント時に1回だけ実行(データ取得に最適) - 依存配列
[dep]: 指定した値が変わるたびに実行 - 依存配列なし: 毎レンダリング後に実行(ほぼ使わない)
- クリーンアップ関数:
return () => { ... }でアンマウント時の後処理 - API取得時は「ローディング・エラー・データ」の3状態を管理する
これで React の基本フック(useState + useEffect)をマスターしました。次は Props と State を組み合わせた実践的なコンポーネント設計を学んでいきましょう!