第8章: フック (Hooks)
フック (Hooks) は React 16.8 で導入された機能で、関数コンポーネントに State やライフサイクルの機能を追加します。
第5章で学んだ useState に加え、副作用を扱う useEffect、
コンテキストを利用する useContext、パフォーマンス最適化のための useCallback / useMemo、
そしてカスタムフックの作り方を解説します。
フックとは
フック (Hooks) とは、関数コンポーネントの中から React の機能にアクセスするための関数です。
名前が use で始まる関数がフックです。
React が提供する主要な組み込みフックは以下の通りです。
| フック | 用途 | 学習済み |
|---|---|---|
useState |
コンポーネントに State(状態)を追加する | 第5章で解説済み |
useEffect |
副作用(データ取得、DOM 操作、タイマーなど)を実行する | この章で解説 |
useRef |
レンダーをまたいで値を保持する / DOM 要素を参照する | この章で解説 |
useContext |
コンテキスト(グローバルな値の共有)を利用する | この章で解説 |
useCallback |
関数をメモ化して不要な再生成を防ぐ | この章で解説 |
useMemo |
計算結果をメモ化して不要な再計算を防ぐ | この章で解説 |
フックのルール
フックを使う際には、必ず以下の 2 つのルールを守る必要があります。 これらのルールに違反すると、React が内部状態を正しく管理できなくなり、バグの原因になります。
ルール 1: トップレベルでのみ呼び出す
フックは if 文、ループ、ネストされた関数の中で呼び出してはいけません。
コンポーネントの関数本体のトップレベルで、常に同じ順序で呼び出す必要があります。
ルール 2: React の関数からのみ呼び出す
フックは React の関数コンポーネントか、カスタムフックの中でのみ呼び出せます。
通常の JavaScript 関数から呼び出すことはできません。
function MyComponent({ isVisible }) {
// OK: トップレベルで呼び出し
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// NG: if 文の中で呼び出し
// if (isVisible) {
// const [data, setData] = useState(null);
// }
// NG: ループの中で呼び出し
// for (let i = 0; i < 3; i++) {
// useEffect(() => { ... });
// }
return <div>...</div>;
}
useEffect
useEffect は、コンポーネントの副作用 (Side Effect) を実行するためのフックです。
副作用とは、レンダリング以外の処理のことで、以下のようなものが含まれます。
- API からのデータ取得
- DOM の直接操作
- タイマーの設定 (
setInterval,setTimeout) - イベントリスナーの登録
- ローカルストレージへのアクセス
基本的な使い方
import { useState, useEffect } from 'react';
function DocumentTitle() {
const [count, setCount] = useState(0);
// レンダーのたびに実行される(依存配列なし)
useEffect(() => {
document.title = `クリック数: ${count}`;
});
return (
<button onClick={() => setCount(count + 1)}>
クリック: {count}
</button>
);
}
依存配列
useEffect の第 2 引数に依存配列 (dependency array) を渡すことで、
副作用を実行するタイミングを制御できます。
| 依存配列 | 実行タイミング | 用途 |
|---|---|---|
| なし(省略) | 毎回のレンダー後 | 通常は使わない(パフォーマンスに注意) |
[](空配列) |
初回レンダー後の 1 回のみ | 初期データの取得、イベントリスナーの登録 |
[value] |
value が変化したとき |
特定の値に応じた処理 |
// パターン 1: 初回のみ実行(マウント時)
useEffect(() => {
console.log('コンポーネントがマウントされました');
}, []); // ← 空の依存配列
// パターン 2: userId が変わるたびに実行
useEffect(() => {
fetchUserData(userId);
}, [userId]); // ← userId が変わったときだけ実行
// パターン 3: 複数の依存値
useEffect(() => {
fetchSearchResults(query, page);
}, [query, page]); // ← query または page が変わったとき
依存配列には、Effect 内で使用するすべてのリアクティブな値を含めてください。
State、Props、それらから計算された値が対象です。依存値を省略すると、
古い値を参照し続けるバグ(stale closure)が発生します。
ESLint の react-hooks/exhaustive-deps ルールが警告してくれるので活用しましょう。
実践例: データ取得
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false; // 古いリクエストの結果を無視するフラグ
async function fetchUser() {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('取得に失敗しました');
const data = await response.json();
if (!ignore) {
setUser(data);
}
} catch (err) {
if (!ignore) {
setError(err.message);
}
} finally {
if (!ignore) {
setLoading(false);
}
}
}
fetchUser();
// クリーンアップ: userId が変わったら古いリクエストを無視
return () => {
ignore = true;
};
}, [userId]);
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error}</p>;
if (!user) return null;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
ignore フラグの重要性:
userId が素早く変更された場合、先に発行したリクエストが後から返ってくることがあります。
ignore フラグを使うことで、古いリクエストの結果を破棄し、
常に最新の userId に対応するデータのみを State にセットできます。
これはレースコンディションを防ぐ定番パターンです。
クリーンアップ関数
useEffect のコールバック関数から関数を返すと、
それがクリーンアップ関数として実行されます。
クリーンアップは以下のタイミングで呼ばれます。
- コンポーネントがアンマウント(画面から消える)されるとき
- 依存値が変更され、次の Effect が実行される直前
クリーンアップが必要な典型的なケースは、イベントリスナーの解除やタイマーのクリアです。
import { useState, useEffect } from 'react';
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
// クリーンアップ: リスナーを解除
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空配列: マウント時に 1 回だけ登録
return (
<p>ウィンドウサイズ: {size.width} x {size.height}</p>
);
}
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// クリーンアップ: タイマーを停止
return () => clearInterval(id);
}, []);
return <p>経過時間: {seconds} 秒</p>;
}
クリーンアップを忘れると「メモリリーク」が発生します。
コンポーネントがアンマウントされた後もイベントリスナーやタイマーが動き続け、
存在しない State を更新しようとしてエラーが発生したり、メモリが無駄に消費されたりします。
addEventListener や setInterval を使う場合は、必ずクリーンアップを実装しましょう。
useRef
useRef は、レンダーをまたいで値を保持するためのフックです。
useState と異なり、値を変更しても再レンダーが発生しません。
主に 2 つの用途で使われます。
用途 1: DOM 要素への参照
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
function handleClick() {
// DOM 要素に直接アクセス
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" placeholder="入力してください" />
<button onClick={handleClick}>フォーカス</button>
</div>
);
}
用途 2: 再レンダーを発生させずに値を保持
import { useState, useEffect, useRef } from 'react';
function RenderCounter() {
const [inputValue, setInputValue] = useState('');
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1; // 再レンダーは発生しない
});
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<p>レンダー回数: {renderCount.current}</p>
</div>
);
}
| 比較項目 | useState | useRef |
|---|---|---|
| 値の変更で再レンダー | する | しない |
| レンダー間で値が保持される | はい | はい |
| DOM 要素への参照 | 不可 | 可能(ref 属性) |
| 値のアクセス方法 | 変数そのもの | .current プロパティ |
useContext
useContext は、コンテキスト (Context) を使ってコンポーネントツリー全体に
値を共有するためのフックです。Props のバケツリレー(prop drilling)を避けることができます。
コンテキストの作成と使用
import { createContext, useContext, useState } from 'react';
// 1. コンテキストを作成
const ThemeContext = createContext('light');
// 2. Provider コンポーネント(値を供給する側)
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Consumer(値を利用する側)
function ThemeButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
現在のテーマ: {theme}(クリックで切り替え)
</button>
);
}
// 4. App で Provider を配置
function App() {
return (
<ThemeProvider>
<div>
<h1>テーマ切り替えデモ</h1>
<ThemeButton />
</div>
</ThemeProvider>
);
}
Context は「テーマ」「認証情報」「言語設定」など、コンポーネントツリー全体で共有する必要がある値に適しています。 頻繁に変化する値(例: 入力中のテキスト)には向きません。Context の値が変わると、 その Context を利用するすべてのコンポーネントが再レンダーされるためです。
useCallback
useCallback は、関数をメモ化して、依存値が変わらない限り同じ関数の参照を返すフックです。
子コンポーネントに関数を渡す際、不要な再レンダーを防ぐのに役立ちます。
import { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// count が変わらない限り、同じ関数の参照を返す
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 依存値なし(setCount は安定した参照)
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>カウント: {count}</p>
{/* text が変わっても ExpensiveChild は再レンダーされない */}
<ExpensiveChild onIncrement={handleIncrement} />
</div>
);
}
useCallback は React.memo と組み合わせて使うことが多いです。
React.memo でラップされた子コンポーネントは、Props が変化しない限り再レンダーをスキップします。
useCallback で関数の参照を安定させることで、この最適化が効果的に機能します。
useMemo
useMemo は、計算結果をメモ化するフックです。
依存値が変わらない限り、前回の計算結果を再利用します。
重い計算処理をレンダーのたびに実行するのを防ぎます。
import { useState, useMemo } from 'react';
function FilteredList({ items }) {
const [query, setQuery] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
// items, query, sortOrder が変わったときだけ再計算
const filteredAndSorted = useMemo(() => {
const filtered = items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return [...filtered].sort((a, b) =>
sortOrder === 'asc'
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
);
}, [items, query, sortOrder]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索..."
/>
<ul>
{filteredAndSorted.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
早すぎる最適化に注意:
useCallback や useMemo は強力ですが、すべての関数や計算に適用すべきではありません。
メモ化にもコスト(メモリ使用量、比較処理)がかかるため、
実際にパフォーマンスの問題が確認された場合にのみ導入しましょう。
多くの場合、React のデフォルトのレンダリングは十分に高速です。
カスタムフック
カスタムフックとは、use で始まる名前を持つ、
フックを内部で使用する通常の JavaScript 関数です。
複数のコンポーネントで共通するロジックをカスタムフックに切り出すことで、
コードの再利用性が大きく向上します。
例: useLocalStorage
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value) => {
const valueToStore = typeof value === 'function'
? value(storedValue)
: value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}
export default useLocalStorage;
import useLocalStorage from './hooks/useLocalStorage';
function Settings() {
const [name, setName] = useLocalStorage('userName', '');
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>テーマ: {theme}</p>
</div>
);
}
例: useFetch
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
if (!ignore) setData(json);
} catch (err) {
if (!ignore) setError(err.message);
} finally {
if (!ignore) setLoading(false);
}
}
fetchData();
return () => { ignore = true; };
}, [url]);
return { data, loading, error };
}
export default useFetch;
import useFetch from './hooks/useFetch';
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
カスタムフックの命名規則は use で始めることです。
これにより ESLint のフックルールが正しく適用されます。
カスタムフック自体は State を「共有」するわけではなく、
State を管理するロジックを共有します。
各コンポーネントは独立した State を持ちます。
フック一覧まとめ
| フック | 用途 | 典型的な使い方 |
|---|---|---|
useState |
State の管理 | フォーム入力、トグル状態、カウンター |
useEffect |
副作用の実行 | API 通信、タイマー、DOM 操作 |
useRef |
値の保持 / DOM 参照 | input へのフォーカス、前回の値の保持 |
useContext |
グローバルな値の共有 | テーマ、認証情報、言語設定 |
useCallback |
関数のメモ化 | 子コンポーネントへの関数渡し |
useMemo |
計算結果のメモ化 | 重いフィルタリング・ソート処理 |
| カスタムフック | ロジックの再利用 | useFetch、useLocalStorage など |