前回の基礎編では、カスタムフックの概念と基本的なパターンを解説しました。今回は、実践的なカスタムフックの作成方法、ベストプラクティス、そしてよくある問題の解決方法を詳しく解説します。
1. カスタムフックのベストプラクティス
1.1 単一責任の原則
1つのカスタムフックは、1つの責任だけを持つべきです。
// ❌ 複数の責任を持つ
function useUserAndTheme() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// ...
}
// ✅ 責任を分離
function useUser() {
const [user, setUser] = useState(null);
// ...
}
function useTheme() {
const [theme, setTheme] = useState('light');
// ...
}
1.2 依存配列の適切な管理
カスタムフック内の useEffect や useMemo の依存配列を正しく指定します。
// ❌ 依存配列が不足
function useFetch(url: string) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, []); // url が変わっても再実行されない!
}
// ✅ 依存配列を正しく指定
function useFetch(url: string) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]); // url が変わったら再実行
}
1.3 クリーンアップの実装
リソースの解放を忘れずに実装します。
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => {
savedCallback.current?.();
}, delay);
return () => clearInterval(id); // ✅ クリーンアップ
}, [delay]);
}
1.4 型安全性の確保
TypeScript を使用する場合、適切な型定義を行います。
// ✅ 型安全なカスタムフック
function useFetch<T>(url: string): {
data: T | null;
loading: boolean;
error: Error | null;
} {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// ...
return { data, loading, error };
}
1.5 カスタムフックのテスト
カスタムフックは renderHook でテストできます。
import { renderHook, act } from '@testing-library/react';
import { useBoolean } from './useBoolean';
test('useBoolean should toggle value', () => {
const { result } = renderHook(() => useBoolean(false));
expect(result.current.value).toBe(false);
act(() => {
result.current.toggle();
});
expect(result.current.value).toBe(true);
});
2. アンチパターンと注意点
2.1 条件付きで Hooks を呼び出す
// ❌ 条件分岐内で Hooks を呼び出す
function useConditionalHook(flag: boolean) {
if (flag) {
const [value, setValue] = useState(0); // エラー!
}
}
// ✅ 常に同じ順序で呼び出す
function useConditionalHook(flag: boolean) {
const [value, setValue] = useState(0);
// flag に基づいて値を返すだけ
return flag ? value : null;
}
2.2 カスタムフックから JSX を返す
// ❌ カスタムフックから JSX を返す(コンポーネントと混同)
function useModal() {
return <Modal />; // これはコンポーネント!
}
// ✅ カスタムフックはロジックのみを返す
function useModal() {
const [isOpen, setIsOpen] = useState(false);
return { isOpen, open: () => setIsOpen(true), close: () => setIsOpen(false) };
}
2.3 過度な抽象化
// ❌ 単純なロジックを無理にカスタムフックにする
function useCounter() {
const [count, setCount] = useState(0);
return { count, setCount };
}
// ✅ シンプルな場合は直接 useState を使う
function Counter() {
const [count, setCount] = useState(0);
// ...
}
2.4 パフォーマンスの過度な最適化
// ❌ 不要な useMemo や useCallback
function useData() {
const [data, setData] = useState(null);
const memoizedData = useMemo(() => data, [data]); // 不要!
return memoizedData;
}
// ✅ シンプルに返す
function useData() {
const [data, setData] = useState(null);
return data;
}
3. 実践的なカスタムフック例
3.1 フォーム管理フック
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const setValue = useCallback(<K extends keyof T>(name: K, value: T[K]) => {
setValues(prev => ({ ...prev, [name]: value }));
// エラーをクリア
if (errors[name]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[name];
return newErrors;
});
}
}, [errors]);
const setError = useCallback(<K extends keyof T>(name: K, error: string) => {
setErrors(prev => ({ ...prev, [name]: error }));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
}, [initialValues]);
return {
values,
errors,
setValue,
setError,
reset,
};
}
// 使用例
function LoginForm() {
const { values, errors, setValue, setError, reset } = useForm({
email: '',
password: '',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!values.email) {
setError('email', 'メールアドレスは必須です');
return;
}
// 送信処理
};
return (
<form onSubmit={handleSubmit}>
<input
value={values.email}
onChange={e => setValue('email', e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
{/* ... */}
</form>
);
}
3.2 非同期処理フック
function useAsync<T, E = Error>() {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: E | null;
}>({
data: null,
loading: false,
error: null,
});
const execute = useCallback(async (asyncFunction: () => Promise<T>) => {
setState({ data: null, loading: true, error: null });
try {
const data = await asyncFunction();
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState({ data: null, loading: false, error: error as E });
throw error;
}
}, []);
return { ...state, execute };
}
// 使用例
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error, execute } = useAsync<User>();
useEffect(() => {
execute(() => fetchUser(userId));
}, [userId, execute]);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error.message}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}
3.3 キーボードショートカットフック
function useKeyboardShortcut(
key: string,
callback: (event: KeyboardEvent) => void,
options: { ctrl?: boolean; shift?: boolean; alt?: boolean } = {}
) {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== key) return;
if (options.ctrl && !event.ctrlKey) return;
if (options.shift && !event.shiftKey) return;
if (options.alt && !event.altKey) return;
callback(event);
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [key, callback, options.ctrl, options.shift, options.alt]);
}
// 使用例
function Editor() {
const [content, setContent] = useState('');
useKeyboardShortcut('s', (e) => {
e.preventDefault();
saveContent(content);
}, { ctrl: true });
return <textarea value={content} onChange={e => setContent(e.target.value)} />;
}
4. カスタムフックライブラリ
コミュニティで人気のあるカスタムフックライブラリ:
4.1 React Hook Form
フォーム管理に特化したライブラリ。
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data: any) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: true })} />
{errors.email && <span>必須です</span>}
<button type="submit">送信</button>
</form>
);
}
4.2 React Query (TanStack Query)
データフェッチングとキャッシングに特化。
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラー</div>;
return <div>{user.name}</div>;
}
4.3 ahooks
豊富なカスタムフックのコレクション。
import { useRequest, useDebounce } from 'ahooks';
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
const { data, loading } = useRequest(() => searchAPI(debouncedQuery), {
refreshDeps: [debouncedQuery],
});
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{loading && <div>検索中...</div>}
{data && <div>{data.map(...)}</div>}
</>
);
}
5. トラブルシューティング
5.1 "Rules of Hooks" エラー
原因: 条件分岐やループ内で Hooks を呼び出している
// ❌ エラー
function useConditional() {
if (condition) {
const [value, setValue] = useState(0);
}
}
// ✅ 修正
function useConditional() {
const [value, setValue] = useState(0);
// 条件に基づいて値を返すだけ
}
5.2 カスタムフックが毎回新しい値を返す
原因: 依存配列が正しく指定されていない、またはオブジェクトが毎回新しく作られている
// ❌ 毎回新しいオブジェクト
function useData() {
const [data, setData] = useState(null);
return { data }; // 毎回新しいオブジェクト
}
// ✅ useMemo でメモ化
function useData() {
const [data, setData] = useState(null);
return useMemo(() => ({ data }), [data]);
}
5.3 メモリリーク
原因: クリーンアップが実装されていない
// ❌ メモリリーク
function useInterval(callback: () => void, delay: number) {
useEffect(() => {
const id = setInterval(callback, delay);
// クリーンアップがない!
}, [callback, delay]);
}
// ✅ クリーンアップを実装
function useInterval(callback: () => void, delay: number) {
useEffect(() => {
const id = setInterval(callback, delay);
return () => clearInterval(id); // クリーンアップ
}, [callback, delay]);
}
6. まとめ
6.1 カスタムフックを使うべき場面
| 場面 | 説明 |
|---|---|
| ロジックの再利用 | 複数のコンポーネントで同じロジックを使う |
| 関心の分離 | UI とビジネスロジックを分離したい |
| テスト容易性 | ロジックを単独でテストしたい |
| 可読性の向上 | コンポーネントをシンプルに保ちたい |
6.2 カスタムフックを作らない方が良い場面
| 場面 | 説明 |
|---|---|
| 1回しか使わないロジック | 無理に抽象化しない |
| 単純な状態管理 |
useState で十分な場合 |
| 過度な抽象化 | 理解が難しくなる場合 |
6.3 重要なポイント
- 単一責任の原則を守る
- 依存配列を正しく管理する
- クリーンアップを忘れない
- 型安全性を確保する
- 過度な抽象化を避ける
カスタムフックは、React アプリケーションのロジックを整理し、再利用可能にする強力なツールです。適切に使用することで、コードの保守性と可読性が大幅に向上します。ただし、過度な抽象化は避け、実際のニーズに基づいて作成することが重要です。