React Hooksは、関数コンポーネントで状態管理や副作用処理を行うための仕組みです。2019年にReact 16.8で導入されて以降、クラスコンポーネントに代わるReact開発の標準となりました。
本記事では、useState、useEffect、useRef、useMemo、useCallbackなど主要なフックの使い方と、再利用可能なカスタムフックの作り方を、実務での具体例とともに解説します。
Hooksの基本ルールと仕組み
Hooksを正しく使うためには、2つの基本ルールを理解しておく必要があります。
ルール1:トップレベルでのみ呼び出す - Hooksは条件分岐、ループ、ネストされた関数の中で呼び出してはいけません。コンポーネントの最上位でのみ使用します。
ルール2:React関数内でのみ呼び出す - Hooksは関数コンポーネントまたはカスタムフック内でのみ使用できます。通常のJavaScript関数では使えません。
// 正しい使い方
function UserProfile() {
const [name, setName] = useState(""); // OK: トップレベル
const [age, setAge] = useState(0); // OK: トップレベル
// 誤った使い方の例
// if (name) {
// const [error, setError] = useState(""); // NG: 条件分岐内
// }
return <div>{name}</div>;
}
Reactは内部的にHooksの呼び出し順序を記録しているため、呼び出し順序が変わると状態の対応が崩れてしまいます。
useStateによる状態管理
useStateは、コンポーネントに状態を持たせるための最も基本的なフックです。
基本的な使い方
import { useState } from "react";
function Counter() {
// [現在の値, 更新関数] = useState(初期値)
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増加</button>
<button onClick={() => setCount(0)}>リセット</button>
</div>
);
}
オブジェクト・配列の状態管理
interface FormData {
name: string;
email: string;
message: string;
}
function ContactForm() {
const [formData, setFormData] = useState<FormData>({
name: "",
email: "",
message: "",
});
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
// スプレッド構文で特定フィールドだけ更新
const handleChange = (field: keyof FormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
// エラーをクリア
setErrors((prev) => ({ ...prev, [field]: undefined }));
};
return (
<form>
<input
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
placeholder="名前"
/>
{errors.name && <span className="error">{errors.name}</span>}
<input
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="メールアドレス"
/>
<textarea
value={formData.message}
onChange={(e) => handleChange("message", e.target.value)}
placeholder="メッセージ"
/>
</form>
);
}
配列のイミュータブルな更新
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
// 追加
const addTodo = (text: string) => {
setTodos((prev) => [
...prev,
{ id: Date.now(), text, completed: false },
]);
};
// 更新(特定のアイテムだけ変更)
const toggleTodo = (id: number) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// 削除
const deleteTodo = (id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>削除</button>
</li>
))}
</ul>
);
}
useEffectによる副作用の処理
useEffectは、データの取得、DOM操作、タイマー、イベントリスナーの登録など、レンダリング以外の処理(副作用)を行うフックです。
依存配列の使い分け
import { useState, useEffect } from "react";
function DataFetchExample({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// 依存配列にuserIdを指定 → userIdが変わるたびに再実行
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) {
setUser(data);
}
} catch (error) {
console.error("Failed to fetch user:", error);
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// クリーンアップ関数:コンポーネントのアンマウント時や依存値の変更時に実行
return () => {
cancelled = true;
};
}, [userId]); // userIdが変わったときだけ再実行
if (loading) return <p>読み込み中...</p>;
if (!user) return <p>ユーザーが見つかりません</p>;
return <p>{user.name}</p>;
}
イベントリスナーの管理
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const 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>
);
}
useEffectの依存配列パターンまとめ
// 1. 毎回のレンダリング後に実行(依存配列なし)
useEffect(() => {
console.log("毎回実行");
});
// 2. マウント時に1回だけ実行(空の依存配列)
useEffect(() => {
console.log("マウント時のみ");
return () => console.log("アンマウント時のクリーンアップ");
}, []);
// 3. 特定の値が変化したときに実行
useEffect(() => {
console.log(`userIdが${userId}に変更`);
}, [userId]);
// 4. 複数の依存値
useEffect(() => {
console.log("pageまたはfilterが変更");
}, [page, filter]);
useRef・useMemo・useCallbackの活用
useRef:再レンダリングを起こさない値の保持
import { useRef, useEffect } from "react";
// DOM要素への参照
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="自動フォーカス" />;
}
// 前回の値を保持
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// タイマーの管理
function StopWatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setTime((prev) => prev + 10);
}, 10);
} else if (intervalRef.current) {
clearInterval(intervalRef.current);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [isRunning]);
return (
<div>
<p>{(time / 1000).toFixed(2)}秒</p>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? "停止" : "開始"}
</button>
<button onClick={() => { setTime(0); setIsRunning(false); }}>
リセット
</button>
</div>
);
}
useMemo:計算結果のメモ化
import { useMemo, useState } from "react";
interface Product {
id: number;
name: string;
price: number;
category: string;
}
function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState("");
const [sortBy, setSortBy] = useState<"name" | "price">("name");
// フィルタリング・ソートの結果をメモ化
// productsかfilterかsortByが変わったときだけ再計算
const filteredProducts = useMemo(() => {
console.log("フィルタリング実行"); // 不要な再計算を検出
return products
.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase()) ||
p.category.toLowerCase().includes(filter.toLowerCase())
)
.sort((a, b) => {
if (sortBy === "price") return a.price - b.price;
return a.name.localeCompare(b.name);
});
}, [products, filter, sortBy]);
// 統計情報のメモ化
const stats = useMemo(() => ({
total: filteredProducts.length,
averagePrice: filteredProducts.length > 0
? Math.floor(
filteredProducts.reduce((sum, p) => sum + p.price, 0) /
filteredProducts.length
)
: 0,
}), [filteredProducts]);
return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="検索" />
<p>{stats.total}件 / 平均価格: ¥{stats.averagePrice}</p>
{filteredProducts.map((product) => (
<div key={product.id}>{product.name}: ¥{product.price}</div>
))}
</div>
);
}
useCallback:関数のメモ化
import { useCallback, useState, memo } from "react";
// memo化された子コンポーネント
const TodoItem = memo(function TodoItem({
todo,
onToggle,
onDelete,
}: {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}) {
console.log(`TodoItem rendered: ${todo.text}`);
return (
<li>
<input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} />
{todo.text}
<button onClick={() => onDelete(todo.id)}>削除</button>
</li>
);
});
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState("");
// useCallbackでメモ化 → TodoItemの不要な再レンダリングを防止
const handleToggle = useCallback((id: number) => {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
}, []);
const handleDelete = useCallback((id: number) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
}, []);
const handleAdd = () => {
if (!input.trim()) return;
setTodos((prev) => [...prev, { id: Date.now(), text: input, completed: false }]);
setInput("");
};
return (
<div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={handleAdd}>追加</button>
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
</div>
);
}
useReducerによる複雑な状態管理
useReducerは、複数の関連する状態をまとめて管理する場合や、状態遷移のロジックが複雑な場合に適しています。
import { useReducer } from "react";
// 状態の型定義
interface FetchState<T> {
data: T | null;
isLoading: boolean;
error: string | null;
}
// アクションの型定義
type FetchAction<T> =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; payload: T }
| { type: "FETCH_ERROR"; payload: string }
| { type: "RESET" };
// リデューサー関数
function fetchReducer<T>(state: FetchState<T>, action: FetchAction<T>): FetchState<T> {
switch (action.type) {
case "FETCH_START":
return { ...state, isLoading: true, error: null };
case "FETCH_SUCCESS":
return { data: action.payload, isLoading: false, error: null };
case "FETCH_ERROR":
return { ...state, isLoading: false, error: action.payload };
case "RESET":
return { data: null, isLoading: false, error: null };
default:
return state;
}
}
// 使用例
function UserProfile({ userId }: { userId: number }) {
const [state, dispatch] = useReducer(fetchReducer<User>, {
data: null,
isLoading: false,
error: null,
});
useEffect(() => {
dispatch({ type: "FETCH_START" });
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => dispatch({ type: "FETCH_SUCCESS", payload: data }))
.catch((err) => dispatch({ type: "FETCH_ERROR", payload: err.message }));
}, [userId]);
if (state.isLoading) return <p>読み込み中...</p>;
if (state.error) return <p>エラー: {state.error}</p>;
if (!state.data) return null;
return <div>{state.data.name}</div>;
}
カスタムフックの作成
カスタムフックは、Hooksのロジックを再利用可能な関数として切り出す仕組みです。名前をuseから始める慣例があります。
useFetch:データ取得フック
import { useState, useEffect, useCallback } from "react";
interface UseFetchResult<T> {
data: T | null;
isLoading: boolean;
error: string | null;
refetch: () => void;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const json = await res.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setIsLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, isLoading, error, refetch: fetchData };
}
// 使用例
function UserList() {
const { data: users, isLoading, error, refetch } = useFetch<User[]>("/api/users");
if (isLoading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error} <button onClick={refetch}>再試行</button></p>;
return (
<ul>
{users?.map((user) => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
useLocalStorage:ローカルストレージと同期するフック
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}
// 使用例
function Settings() {
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
const [fontSize, setFontSize] = useLocalStorage("fontSize", 16);
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value as "light" | "dark")}>
<option value="light">ライト</option>
<option value="dark">ダーク</option>
</select>
<input
type="range"
min={12}
max={24}
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
/>
</div>
);
}
useDebounce:入力の遅延処理フック
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 使用例:検索入力のデバウンス
function SearchBox() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
const { data: results } = useFetch<Product[]>(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : ""
);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="検索..." />
{results?.map((r) => <div key={r.id}>{r.name}</div>)}
</div>
);
}
まとめ
React Hooksは、関数コンポーネントに状態管理と副作用処理の能力を与える仕組みです。各フックの使い分けを整理します。
useState:コンポーネントの状態を管理します。オブジェクトや配列はイミュータブルに更新することが重要です。
useEffect:データ取得、イベントリスナー、タイマーなどの副作用を処理します。クリーンアップ関数でリソースリークを防ぎます。
useRef:DOM参照や、レンダリングを起こさない値の保持に使います。
useMemo・useCallback:計算結果や関数をメモ化してパフォーマンスを最適化します。ただし、不要な場面での使用は避けましょう。
useReducer:複雑な状態遷移をリデューサー関数で管理します。useStateでは扱いきれない状態に有効です。
カスタムフック:ロジックを再利用可能な関数に切り出します。useFetch、useLocalStorage、useDebounceなど、プロジェクト共通のフックを作ることで開発効率が上がります。
まずはuseStateとuseEffectを確実に使いこなし、必要に応じてuseMemo・useCallback・カスタムフックを導入していくのが効果的です。
関連記事
AIエージェント開発入門|自律型AIの仕組みと構築方法を解説【2026年版】
AI駆動コーディングワークフロー|Claude Code・Cursor・Copilotの実践的使い分け
プロンプトエンジニアリング上級編|Chain-of-Thought・Few-Shot・ReActの実践
APIレート制限の設計と実装|トークンバケット・スライディングウィンドウ解説
APIバージョニング戦略|URL・ヘッダー・クエリパラメータの使い分け
BIツール入門|Metabase・Redash・Looker Studioでデータ可視化する方法
チャットボット開発入門|LINE Bot・Slack Botの構築方法と活用事例
CI/CDパイプラインの基礎|継続的インテグレーション・デリバリーの全体像