C
React/Hooks/Lesson 12

カスタム Hooks

30分·theory
このチャプター
4/5
JavaScript

カスタム Hooks

💡 なぜ学ぶべきなのか?

🎯 同じロジックを複数のコンポーネントで繰り返し記述せずに再利用できます。
💼 複雑なコンポーネントをシンプルにし、コードを読みやすくします。
チームプロジェクトで開発速度を大幅に向上させる高度なテクニックです。
🏢 실무에서는
実務では「ユーザー情報の取得」「フォームのバリデーション」「ローカルストレージの管理」といった繰り返されるロジックをカスタムHookとして作成し、複数のページで再利用します。大規模なチームプロジェクトでは、共通のHookライブラリを作成して全員が一緒に活用します。

概念

カスタムフックは、コンポーネントから状態ロジックを分離して再利用可能でテスト可能な単位にする、React 19 のコアパターンです。実務においてコンポーネントの複雑さを下げ、チーム全体の生産性を高めるために欠かせない技術です。

なぜ重要か?

大規模なサービスでは、複数のコンポーネントが同一の状態ロジック(API 呼び出し、フォームバリデーション、localStorage など)を繰り返し実装すると、コードの重複とバグが急増します。たとえばログイン状態管理ロジックが 10 個のコンポーネントに分散していると、認証方式を変更する際に 10 か所すべてを修正しなければなりません。

コアコンセプト

カスタムフックは共通の道具箱のようなものです。各部屋にドライバーやレンチを別々に置くのではなく一つの道具箱にまとめるように、複数のコンポーネントで使われる状態ロジックをカスタムフックとして切り出し、一元管理します。

ポイント

  • use で始まる関数名で React 組み込みフックを組み合わせて作成
  • 状態と状態操作関数をオブジェクトまたは配列で返して再利用性を最大化
  • コンポーネントのロジックから完全に分離されているため、ユニットテストが非常に容易
💻 アンチパターン — コンポーネント内にすべてのロジックを含める
function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch('/api/user');
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError('ユーザー情報を読み込めません');
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, []);

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  return <div>{user?.name}</div>;
}
💻 2025 年推奨パターン — テスト可能なカスタムフック
// hooks/useApi.ts
function useApi<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : '不明なエラー');
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData } as const;
}

// コンポーネントでの使用
function UserProfile() {
  const { data: user, loading, error } = useApi<User>('/api/user');

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  return <div>{user?.name}</div>;
}

// __tests__/useApi.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from '../hooks/useApi';

test('API 呼び出し成功時にデータを返す', async () => {
  global.fetch = jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ id: 1, name: '山田太郎' })
  });

  const { result } = renderHook(() => useApi('/api/user'));

  await waitFor(() => {
    expect(result.current.data).toEqual({ id: 1, name: '山田太郎' });
  });
});
💻 実務パターン — フォーム管理カスタムフック
// hooks/useForm.ts
type ValidationRule<T> = {
  [K in keyof T]?: (value: T[K]) => string | null;
};

function useForm<T extends Record<string, any>>(
  initialValues: T,
  validationRules?: ValidationRule<T>
) { 
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});

  const setValue = useCallback((name: keyof T, value: any) => {
    setValues(prev => ({ ...prev, [name]: value }));
    
    // リアルタイム検証
    if (validationRules?.[name]) {
      const error = validationRules[name](value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  }, [validationRules]);

  const setTouched = useCallback((name: keyof T) => {
    setTouched(prev => ({ ...prev, [name]: true }));
  }, []);

  const isValid = useMemo(() => {
    return Object.values(errors).every(error => !error);
  }, [errors]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    setValue,
    setTouched,
    isValid,
    reset
  } as const;
}

// 使用例
function LoginForm() { 
  const { values, errors, setValue, isValid } = useForm(
    { email: '', password: '' },
    {
      email: (value) => !value.includes('@') ? 'メールアドレスの形式が正しくありません' : null,
      password: (value) => value.length < 8 ? 'パスワードは8文字以上である必要があります' : null
    }
  );

  return (
    <form>
      <input 
        value={values.email}
        onChange={(e) => setValue('email', e.target.value)}
      />
      {errors.email && <span>{errors.email}</span>}
      
      <button type="submit" disabled={!isValid}>
        ログイン
      </button>
    </form>
  );
}

💡 ⚠️ よくある間違い

  • フック内部で条件付きに別のフックを呼び出し、React フックのルールに違反する(常に同じ順序でフックを呼び出す必要がある)
  • 依存配列の設定を誤って無限再レンダリングを引き起こす(useCallbackuseEffect の依存関係管理のミス)
  • 一つのカスタムフックに責務を詰め込みすぎて単一責任原則に違反する(useEverything のような巨大フック)

💡 🎯 面接対策

Q: カスタムフックと通常の関数の違いは何ですか?
Q: カスタムフックはどのようにテストしますか?
Q: いつカスタムフックを作るべきだと考えますか?

ヒント: 1) カスタムフックは React 組み込みフックを使用でき、コンポーネントのライフサイクルに参加できる。 2) renderHookact を使ったユニットテストが可能。 3) 2 つ以上のコンポーネントで同一の状態ロジックが繰り返される場合に切り出しを検討する。

⚛️ React パターン — カスタム Hooks

カスタム Hooks を React でどのように使うか、コードとともにステップごとに学びましょう。
1 📦 1. State の宣言
`useState` でコンポーネント内部の状態を作る
const [count, setCount] = useState(0);
2 👁️ 2. レンダリング
state の値を JSX に表示する
return <h1>現在のカウント: {count}</h1>;
3 🔄 3. 更新
`setState` を呼び出す → 再レンダリングをトリガー
<button onClick={() => setCount(count + 1)}>+1</button>
4 💡 4. コア原則
state はイミュータブル — 直接変更は禁止、常にセッターを使う

🎮 カスタム Hooks — ステップごとに理解する

各ステップをクリックして内容を読み、✓ 理解しました ボタンで進捗を確認してください。
🖥️ 実行結果 — レンダリングされたReactコンポーネント
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — まず結果を確認し、エディタで自由に編集できます。

確認クイズ

カスタム Hook を作る際に守るべき命名規則とは何ですか?
💡 カスタムHookは必ず「use」で始まる必要があります。この規則により、ReactはeslintのHookルール違反をeslint-plugin-react-hooksで自動検出できます。
先に読むとよい概念: SyntheticEvent — イベントタイプ
次のおすすめ: useContext
カスタム Hooks - React