Reactのカスタムフック入門 — ロジックを再利用する
はじめに
React入門シリーズ7回目。今回は カスタムフック。
useState / useEffect / useContext と「使うだけ」のフックを学んできましたが、いよいよ「自分で作る側」に回ります。
カスタムフックは、「複数のコンポーネントで使い回したいロジックを、関数として切り出す」 ための仕組み。
Javaエンジニアへの橋渡し: 「Spring の Service 層に切り出すのと同じ発想」 — UI から離して、再利用可能なロジック単位にまとめる、それだけです。
1. なぜカスタムフックが必要か
例えば、複数の画面で「ローカルストレージから値を読んで、変わったら保存する」処理を書きたい時。
普通に書くとこんな感じになります:
function SettingsPage() {
const [theme, setTheme] = useState(() =>
localStorage.getItem("theme") ?? "light"
);
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>切替</button>;
}
function UserPreferencesPage() {
const [lang, setLang] = useState(() =>
localStorage.getItem("lang") ?? "ja"
);
useEffect(() => {
localStorage.setItem("lang", lang);
}, [lang]);
// ...
}
ほぼ同じパターンが2回出てきます。これがあと10画面増えたら……地獄。
「使い回したいけど useState/useEffect が混じってるから関数に切り出せない」 という葛藤を解決するのが、カスタムフック。
2. カスタムフックの作り方(最小例)
import { useState, useEffect } from "react";
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
これだけ。「use〜 で始まる関数」を自分で作る、それがカスタムフックです。
使う側
function SettingsPage() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>切替</button>;
}
function UserPreferencesPage() {
const [lang, setLang] = useLocalStorage("lang", "ja");
return <button onClick={() => setLang(lang === "ja" ? "en" : "ja")}>切替</button>;
}
1行で済む。useState を使う感覚と全く同じ。
3. カスタムフックの3つのルール
ルール1: 関数名は必ず use〜 で始める
function useFoo() { ... } // ✅ カスタムフック
function getFoo() { ... } // ❌ ただの関数(フックを呼べない)
なぜなら、React は use〜 で始まる関数を「フック」と認識して、フックのルール(後述)を適用するから。ESLint プラグインもこれを基準に静的解析しています。
ルール2: フックの呼び出しは「コンポーネントかカスタムフックの中」だけ
function regularFunction() {
const [n, setN] = useState(0); // ❌ 普通の関数の中ではダメ
}
useState / useEffect などのフックは、React 関数の中でしか呼べない。これがあるからカスタムフックを use〜 命名にする意味があります。
ルール3: 条件分岐の中でフックを呼ばない
function MyComponent({ flag }) {
if (flag) {
const [n, setN] = useState(0); // ❌ 条件付き呼び出し
}
}
React は 「フックが呼ばれる順番」 で内部の状態を管理しているので、毎回同じ順番で呼ばないと壊れます。
これらは React の 「Rules of Hooks」 と呼ばれていて、ESLint の react-hooks プラグインで自動チェックできます。導入しておくと安全。
4. 実例集
4-1. useToggle — ON/OFF を切り替える
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(v => !v);
return [value, toggle] as const;
}
// 使い方
function Modal() {
const [isOpen, toggle] = useToggle();
return (
<>
<button onClick={toggle}>開く</button>
{isOpen && <div>モーダル中身</div>}
</>
);
}
10行のフックですが、地味に何度も使います。
4-2. useFetch — API 取得を共通化
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(json => {
if (!cancelled) {
setData(json);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// 使い方
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useFetch<User>(`/api/users/${userId}`);
if (loading) return <p>読み込み中…</p>;
if (error) return <p>エラー: {error.message}</p>;
return <h1>{data?.name}</h1>;
}
useFetch で データ取得・ローディング・エラー処理をひとまとめに。同じ流れを全画面で書かずに済みます。
実務では SWR / React Query などのライブラリを使うのが定番ですが、「中身は同じ構造のカスタムフック」 だと思って読むと、ライブラリの理解も早くなります。
4-3. useDebounce — 入力遅延を実装
function useDebounce<T>(value: T, delay: number) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
// 使い方: 検索ボックスで打つたびにAPI叩かないようにする
function SearchBox() {
const [keyword, setKeyword] = useState("");
const debounced = useDebounce(keyword, 300);
useEffect(() => {
if (debounced) {
fetch(`/api/search?q=${debounced}`);
}
}, [debounced]);
return <input value={keyword} onChange={e => setKeyword(e.target.value)} />;
}
「入力中はAPIを叩かず、止まったら投げる」という挙動が10行で書ける。
5. 設計のコツ
5-1. 返り値は配列より「オブジェクト」が読みやすい
// 配列(useState っぽい)
const [data, loading, error] = useFetch(url);
// オブジェクト(呼び出し側が読みやすい)
const { data, loading, error } = useFetch(url);
返り値が3個以上ならオブジェクトにすると、「何が何だっけ」と迷わない。
5-2. 1つのフックに詰め込みすぎない
// ❌ 何でも入れたフック
useUserDashboard() // ユーザー取得 + 注文取得 + 通知購読 + ...
// ✅ 役割を絞る
useUser()
useOrders()
useNotifications()
「単一責任の原則(SRP)」はカスタムフックにも適用される。Java の Service クラスと同じ感覚で。
5-3. 依存配列は明示的に
function useFetch(url: string) {
useEffect(() => {
// ...
}, [url]); // ← 依存は必ず正しく
}
ESLint の exhaustive-deps ルールが警告を出すので、それに従えば基本OK。
6. Java エンジニアへの橋渡し
| React | Java |
|---|---|
| カスタムフック | Service クラス |
useState を呼ぶ |
フィールドを持つ |
useEffect を呼ぶ |
副作用メソッドを書く |
| 戻り値 | メソッドの戻り値 |
| Rules of Hooks | Spring の Bean ライフサイクル制約 |
「UI から離して、再利用可能なロジック単位にまとめる」というのは、レイヤーアーキテクチャと同じ思想。
UI ばかり書いてるとロジックがコンポーネントに張り付いてしまうけど、「これは Service に切り出すぞ」のノリでカスタムフック化すると、コードが急に整理されます。
7. 実務での選び方ガイド
| シーン | 作るべきか? |
|---|---|
| 2か所以上で同じパターンが出てきた | ✅ すぐに作る |
| useState + useEffect の組み合わせを毎回書いてる | ✅ 作る |
| 1か所でしか使わない | ❌ まだ作らない |
| ロジックが10行未満 | ❌ コンポーネント内で OK |
「先回り抽象化」は React でも避ける。同じパターンが2回出てから初めて切り出す、くらいがちょうどいい。
まとめ
| 項目 | ポイント |
|---|---|
| カスタムフック |
use〜 で始まる、フックを呼べる自作関数 |
| 何のため | ロジックの再利用、useState/useEffect の共通化 |
| ルール | use命名、コンポーネント内、条件分岐NG |
| 設計 | 返り値はオブジェクト、単一責任、依存配列を正しく |
| 作るタイミング | 2か所目に登場した時 |
おわりに
カスタムフックを使えるようになると、React の世界では 「ロジックを書く側」 に立てます。
useState や useEffect が「部品を使う側」だとしたら、カスタムフックは「部品を組み立てる側」。
ライブラリの中身(SWR / React Query / React Hook Form など)は、ほぼ全部カスタムフックの集合体です。カスタムフックが書ける = ライブラリが読める に直結します。
次回はいよいよ React と TypeScript。型をつけて、コンポーネントを安全に書く話を整理します。React 入門編もあと2回でゴール!