0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Hooks を Hackしよう!【Part24: カスタムフック実践編】

Last updated at Posted at 2025-12-25

前回の基礎編では、カスタムフックの概念と基本的なパターンを解説しました。今回は、実践的なカスタムフックの作成方法、ベストプラクティス、そしてよくある問題の解決方法を詳しく解説します。

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 依存配列の適切な管理

カスタムフック内の useEffectuseMemo の依存配列を正しく指定します。

// ❌ 依存配列が不足
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 重要なポイント

  1. 単一責任の原則を守る
  2. 依存配列を正しく管理する
  3. クリーンアップを忘れない
  4. 型安全性を確保する
  5. 過度な抽象化を避ける

カスタムフックは、React アプリケーションのロジックを整理し、再利用可能にする強力なツールです。適切に使用することで、コードの保守性と可読性が大幅に向上します。ただし、過度な抽象化は避け、実際のニーズに基づいて作成することが重要です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?