React を使っていると、「このロジック、他のコンポーネントでも使いたいな...」と思うことがあります。そんなとき、既存の Hooks を組み合わせて独自のロジックを作成できるのが**カスタムフック(Custom Hooks)**です。本記事では、カスタムフックの基礎と内部構造を解説します。
検証環境: この記事は facebook/react リポジトリ(2025年12月時点の main ブランチ)の実際のソースコードを参照して書かれています。
1. なぜカスタムフックが必要か
1.1 ロジックの重複問題
複数のコンポーネントで同じロジックを使う場合、従来は以下のような問題がありました:
// ❌ ロジックが重複
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connect', () => setIsConnected(true));
connection.on('message', (msg) => setMessages(prev => [...prev, msg]));
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <div>...</div>;
}
function AnotherChatRoom({ roomId }) {
// 同じロジックをまた書く必要がある...
const [messages, setMessages] = useState([]);
// ...
}
問題点:
- 同じロジックが複数のコンポーネントに散在
- バグ修正や機能追加の際に複数箇所を修正する必要がある
- テストが困難
1.2 カスタムフックによる解決
カスタムフックを使うと、ロジックを1箇所に集約し、複数のコンポーネントで再利用できます:
// ✅ カスタムフックに抽出
function useChatRoom(roomId: string) {
const [messages, setMessages] = useState<Message[]>([]);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connect', () => setIsConnected(true));
connection.on('message', (msg) => setMessages(prev => [...prev, msg]));
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return { messages, isConnected };
}
// 使用側
function ChatRoom({ roomId }) {
const { messages, isConnected } = useChatRoom(roomId);
return <div>...</div>;
}
1.3 カスタムフックの定義
カスタムフックは、use で始まる関数です。React の組み込み Hooks を呼び出すことができます。
function useCustomHook(initialValue: number) {
const [value, setValue] = useState(initialValue);
useEffect(() => {
// 何らかの副作用
}, [value]);
return { value, setValue };
}
重要なルール:
- 関数名は
useで始まる必要がある(規約) - 他の Hooks を呼び出すことができる
- 任意の値を返すことができる
💡 use で始まらない関数では Hooks を使えない?
技術的には、use で始まらない関数でも React の組み込み Hooks(useState、useEffect など)を呼び出すことは可能です。しかし、ESLint の react-hooks/rules-of-hooks ルールが警告を出します。
// ❌ ESLint が警告を出す(技術的には動作する)
function customHook(initialValue: number) {
const [value, setValue] = useState(initialValue); // 警告: React Hook "useState" is called in function "customHook" that is neither a React function component nor a custom React Hook function
return { value, setValue };
}
// ✅ 推奨される書き方
function useCustomHook(initialValue: number) {
const [value, setValue] = useState(initialValue);
return { value, setValue };
}
なぜ use で始める必要があるのか?
- ESLint が Hooks のルール違反を検出できる
- コードの可読性が向上する(カスタムフックであることが明確)
- React DevTools でカスタムフックを識別できる
1.4 カスタムフックの利点
| 利点 | 説明 |
|---|---|
| ロジックの再利用 | 複数のコンポーネントで同じロジックを共有 |
| 関心の分離 | UI ロジックとビジネスロジックを分離 |
| テスト容易性 | カスタムフックを単独でテスト可能 |
| 可読性の向上 | コンポーネントがシンプルになり、意図が明確に |
2. カスタムフックの内部構造
カスタムフックは、実は特別な実装はありません。通常の関数として実装され、React の Hooks ルールに従って動作します。
2.1 全体像
📦 カスタムフック関数(useXXX)
↓
🎣 内部で React Hooks を呼び出す
↓
🔗 Hook チェーンに追加される
↓
📤 値を返す(任意の型)
カスタムフックは、呼び出し元のコンポーネントのHook チェーンに直接追加されます。
2.2 実行コンテキスト
カスタムフックが呼ばれると、React はそれを呼び出し元のコンポーネントの一部として扱います:
function MyComponent() {
const [count, setCount] = useState(0); // Hook #1
const { value } = useCustomHook(10); // Hook #2, #3 (useState, useEffect)
const [name, setName] = useState(''); // Hook #4
return <div>...</div>;
}
内部的な処理:
// packages/react-reconciler/src/ReactFiberHooks.js
function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: Function,
props: any,
...
) {
// Dispatcher を設定
ReactSharedInternals.H = /* マウント or 更新 */;
// コンポーネントを実行(この中でカスタムフックも呼ばれる)
let children = Component(props);
// Dispatcher をリセット
ReactSharedInternals.H = ContextOnlyDispatcher;
return children;
}
カスタムフック内で呼ばれる useState や useEffect は、呼び出し元のコンポーネントの Hook チェーンに追加されます。
2.3 Hook チェーンへの追加
function useCounter(initialValue: number) {
const [count, setCount] = useState(initialValue); // ← Hook チェーンに追加
return { count, setCount };
}
function MyComponent() {
const [name, setName] = useState(''); // Hook #1
const { count, setCount } = useCounter(0); // Hook #2 (useCounter 内の useState)
const [age, setAge] = useState(0); // Hook #3
}
内部的な Hook チェーン構造:
Fiber.memoizedState
↓
Hook1 (useState: name)
↓
Hook2 (useState: count) ← useCounter 内の useState
↓
Hook3 (useState: age)
↓
null
2.4 カスタムフックのルール
カスタムフックも、通常の Hooks と同じルールに従う必要があります:
ルール1: トップレベルでのみ呼び出す
// ❌ 条件分岐内で呼び出せない
function MyComponent({ flag }) {
if (flag) {
const { value } = useCustomHook(); // エラー!
}
}
// ✅ トップレベルで呼び出す
function MyComponent({ flag }) {
const { value } = useCustomHook();
if (flag) {
// value を使う
}
}
ルール2: ループ内で呼び出せない
// ❌ ループ内で呼び出せない
function MyComponent({ items }) {
return items.map(item => {
const { value } = useCustomHook(item); // エラー!
return <div>{value}</div>;
});
}
// ✅ コンポーネントを分離
function Item({ item }) {
const { value } = useCustomHook(item);
return <div>{value}</div>;
}
2.5 カスタムフックの識別
React は、カスタムフックを特別には扱いません。use で始まる関数名は、開発者とツール(ESLint など)のための規約です。
// packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
function isHookName(name: string): boolean {
return /^use[A-Z0-9]/.test(name);
}
ESLint の react-hooks/rules-of-hooks ルールは、use で始まる関数内でのみ Hooks を呼び出すことを強制します。
2.6 実行フロー
3. 代表的なカスタムフックパターン
3.1 状態管理フック
状態とその更新ロジックをカプセル化します。
function useBoolean(initialValue: boolean = false) {
const [value, setValue] = useState(initialValue);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
const toggle = useCallback(() => setValue(v => !v), []);
return { value, setValue, setTrue, setFalse, toggle };
}
// 使用例
function Modal() {
const { value: isOpen, setTrue: open, setFalse: close } = useBoolean();
return (
<>
<button onClick={open}>開く</button>
{isOpen && (
<div>
<p>モーダル</p>
<button onClick={close}>閉じる</button>
</div>
)}
</>
);
}
3.2 副作用フック
外部システムとの同期をカプセル化します。
function useWindowSize() {
const [size, setSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
3.3 データフェッチングフック
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 ignore = false;
setLoading(true);
setError(null);
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(json => {
if (!ignore) {
setData(json);
setLoading(false);
}
})
.catch(err => {
if (!ignore) {
setError(err);
setLoading(false);
}
});
return () => {
ignore = true;
};
}, [url]);
return { data, loading, error };
}
3.4 前回値の保持フック
前回のレンダー時の値を保持します。
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
3.5 デバウンスフック
ユーザー入力の最適化に使用します。
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
3.6 ローカルストレージ同期フック
ローカルストレージと状態を同期します。
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
4. まとめ
- カスタムフックは
useで始まる関数 - 内部で React Hooks を呼び出すことができる
- 呼び出し元のコンポーネントの Hook チェーンに追加される
- Hooks のルール(トップレベル、条件分岐禁止)に従う必要がある
- 任意の値を返すことができる
カスタムフックは、React アプリケーションのロジックを整理し、再利用可能にする強力なツールです。次回の実践編では、ベストプラクティスと実践的なパターンを詳しく解説します。