Reactカスタムフック入門|ロジックを再利用する基本パターン

Reactカスタムフックの基本的な作り方から実践的な活用例まで解説。ロジックの再利用によってコードの保守性と可読性を向上させる方法を詳しく紹介

Learning Next 運営
91 分で読めます

みなさん、Reactを使っていて「同じコードを何度も書いてる気がする」と感じたことはありませんか?

「APIの呼び出しが似たような感じで、何度も書いてる」 「状態管理のロジックが複数のコンポーネントで重複してる」

こんな経験、ありますよね。

そんな時に活躍するのがカスタムフックです。 これを使うと、同じロジックを何度も書く必要がなくなります。

この記事では、カスタムフックの基本から実践的な使い方まで、初心者にもわかりやすく解説します。 コードの保守性と可読性を向上させるコツも一緒にお伝えしますので、ぜひ参考にしてください。

カスタムフックとは

カスタムフックって、聞いたことありますか?

簡単に言うと、状態管理のロジックを再利用できる関数のことです。 複数のコンポーネントで同じようなロジックを使いたい時に、とても便利な機能なんです。

基本的な概念

まずは、カスタムフックがどんなものかを理解しましょう。

// ❌ 同じロジックを複数のコンポーネントで重複
function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUser()
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
}

function UserSettings() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUser()
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>Settings for {user?.name}</div>;
}

上のコードを見てください。 同じようなロジックが2回も書かれていますね。

これがカスタムフックを使うと、こんなに簡潔になります。

// ✅ カスタムフックで共通ロジックを抽象化
function useUser() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUser()
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  return { user, loading, error };
}

// 簡潔になったコンポーネント
function UserProfile() {
  const { user, loading, error } = useUser();

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
}

function UserSettings() {
  const { user, loading, error } = useUser();

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>Settings for {user?.name}</div>;
}

どうでしょうか? すごくスッキリしましたよね。

このuseUserがカスタムフックです。 共通のロジックを1つの関数にまとめることで、コードの重複を避けることができます。

カスタムフックのルール

カスタムフックを作る時は、いくつかのルールがあります。

// ✅ 正しいカスタムフック
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  const decrement = useCallback(() => {
    setCount(prev => prev - 1);
  }, []);

  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

  return { count, increment, decrement, reset };
}

// ❌ 間違った使用例
function NotAHook() {
  // 関数名が"use"で始まらない
  const [state, setState] = useState(0); // Hooksを使用しているのに命名規則違反
  return state;
}

function useInvalidHook() {
  if (Math.random() > 0.5) {
    // 条件分岐内でHooksを使用(ルール違反)
    const [state, setState] = useState(0);
  }
  return null;
}

ルールはとてもシンプルです。

1. 関数名は必ず「use」で始める これはReactの決まりごとです。 useCounteruseToggleのように、useで始めてください。

2. Hooksは条件分岐の中で使わない useStateuseEffectは、必ず関数のトップレベルで使いましょう。

3. 必ず値を返す カスタムフックは、コンポーネントが使うデータや関数を返します。

これだけ覚えておけば大丈夫です。

カスタムフックのメリット

カスタムフックを使うと、どんな良いことがあるのでしょうか?

// メリット1: ロジックの再利用
const useToggle = (initialValue = false) => {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(prev => !prev);
  }, []);

  return [value, toggle];
};

// 複数の場所で使用可能
function Modal() {
  const [isOpen, toggleOpen] = useToggle(false);
  return (
    <div>
      <button onClick={toggleOpen}>Open Modal</button>
      {isOpen && <div>Modal Content</div>}
    </div>
  );
}

function Sidebar() {
  const [isExpanded, toggleExpanded] = useToggle(true);
  return (
    <div>
      <button onClick={toggleExpanded}>Toggle Sidebar</button>
      <aside style={{ width: isExpanded ? '200px' : '50px' }}>
        Sidebar
      </aside>
    </div>
  );
}

このuseToggleフックは、オン・オフの切り替えが必要な場面で使えます。 モーダルの開閉、サイドバーの表示・非表示など、いろんな場所で活用できますね。

// メリット2: テストの容易さ
// カスタムフックは単体でテストできる
import { renderHook, act } from '@testing-library/react';

test('useToggle should toggle value', () => {
  const { result } = renderHook(() => useToggle(false));
  
  expect(result.current[0]).toBe(false);
  
  act(() => {
    result.current[1]();
  });
  
  expect(result.current[0]).toBe(true);
});

カスタムフックは、コンポーネントとは別にテストできます。 これにより、バグの発見が早くなり、コードの品質が向上します。

// メリット3: 関心の分離
// コンポーネントはUIのみに集中できる
function TodoApp() {
  const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();
  const { filter, setFilter, filteredTodos } = useTodoFilter(todos);

  // UIロジックのみに集中
  return (
    <div>
      <TodoInput onAdd={addTodo} />
      <TodoFilter filter={filter} onFilterChange={setFilter} />
      <TodoList 
        todos={filteredTodos} 
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
    </div>
  );
}

このTodoAppコンポーネントは、UIの表示だけに集中できています。 データの管理はカスタムフックが担当しているので、コンポーネントがとてもシンプルになりました。

まとめると、カスタムフックのメリットは以下の通りです:

  • ロジックの再利用:同じ機能を複数の場所で使える
  • テストの容易さ:ロジックを独立してテストできる
  • 関心の分離:UIとロジックを分けて考えられる

次は、実際によく使われるカスタムフックのパターンを見てみましょう。

基本的なカスタムフックパターン

実際のアプリケーションでよく使われるパターンを紹介します。

状態管理パターン

シンプルな状態管理を抽象化するパターンです。

// ローカルストレージと同期する状態管理
function useLocalStorage(key, initialValue) {
  // 初期値を取得
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // 値を設定する関数
  const setValue = useCallback((value) => {
    try {
      // 関数が渡された場合は実行
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);

  // 値を削除する関数
  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
}

このuseLocalStorageフックは、ブラウザのローカルストレージと連携します。

どんな処理をしているか詳しく見てみましょう:

最初に、ローカルストレージから値を取得します。 見つからない場合は、初期値を使用します。

setValue関数で値を更新すると、自動的にローカルストレージにも保存されます。 removeValue関数で値を削除することもできます。

// 使用例
function Settings() {
  const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'ja');

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">ライト</option>
        <option value="dark">ダーク</option>
      </select>
      
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="ja">日本語</option>
        <option value="en">English</option>
      </select>
      
      <button onClick={removeTheme}>テーマをリセット</button>
    </div>
  );
}

使い方はとても簡単です。 普通のuseStateと同じように使えますが、値が自動的にローカルストレージに保存されます。

ページをリロードしても、設定値が保持されているので便利ですね。

// カウンター機能のカスタムフック
function useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount(prev => prev + step);
  }, [step]);

  const decrement = useCallback(() => {
    setCount(prev => prev - step);
  }, [step]);

  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

  const setValue = useCallback((value) => {
    setCount(typeof value === 'function' ? value : value);
  }, []);

  return {
    count,
    increment,
    decrement,
    reset,
    setValue,
    // 便利なプロパティ
    isZero: count === 0,
    isNegative: count < 0,
    isPositive: count > 0,
  };
}

このuseCounterフックは、カウンターの機能を提供します。

主な機能:

  • increment:値を増やす
  • decrement:値を減らす
  • reset:初期値に戻す
  • setValue:直接値を設定する

さらに、isZeroisPositiveなどの便利なプロパティも含まれています。

// 使用例
function CounterApp() {
  const { count, increment, decrement, reset, isZero } = useCounter(0, 2);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+2</button>
      <button onClick={decrement}>-2</button>
      <button onClick={reset} disabled={isZero}>
        Reset
      </button>
    </div>
  );
}

このカウンターは、2ずつ増減します。 isZeroを使って、値が0の時はリセットボタンを無効にしています。

// トグル機能のカスタムフック
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(prev => !prev);
  }, []);

  const setTrue = useCallback(() => {
    setValue(true);
  }, []);

  const setFalse = useCallback(() => {
    setValue(false);
  }, []);

  return {
    value,
    toggle,
    setTrue,
    setFalse,
    setValue,
  };
}

このuseToggleフックは、true/falseの切り替えを簡単にします。

主な機能:

  • toggle:値を反転させる
  • setTrue:trueに設定
  • setFalse:falseに設定
// 使用例
function ToggleDemo() {
  const modal = useToggle(false);
  const sidebar = useToggle(true);

  return (
    <div>
      <button onClick={modal.toggle}>
        {modal.value ? 'Close' : 'Open'} Modal
      </button>
      
      <button onClick={sidebar.toggle}>
        {sidebar.value ? 'Hide' : 'Show'} Sidebar
      </button>

      {modal.value && (
        <div className="modal">
          <p>Modal Content</p>
          <button onClick={modal.setFalse}>Close</button>
        </div>
      )}

      <aside style={{ display: sidebar.value ? 'block' : 'none' }}>
        Sidebar Content
      </aside>
    </div>
  );
}

モーダルの開閉やサイドバーの表示・非表示など、いろんな場面で活用できます。

フォーム管理パターン

フォームの状態管理とバリデーションを抽象化します。

// 基本的なフォーム管理フック
function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // フィールドの値を更新
  const setValue = useCallback((name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));
    
    // エラーをクリア
    if (errors[name]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
  }, [errors]);

  // フィールドがタッチされたことを記録
  const setTouched = useCallback((name) => {
    setTouched(prev => ({ ...prev, [name]: true }));
  }, []);

  // エラーを設定
  const setFieldError = useCallback((name, error) => {
    setErrors(prev => ({ ...prev, [name]: error }));
  }, []);

  // フォームをリセット
  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  // フィールドの変更ハンドラ
  const handleChange = useCallback((e) => {
    const { name, value, type, checked } = e.target;
    setValue(name, type === 'checkbox' ? checked : value);
  }, [setValue]);

  // フィールドのblurハンドラ
  const handleBlur = useCallback((e) => {
    setTouched(e.target.name);
  }, [setTouched]);

  // バリデーション
  const validate = useCallback((validationRules) => {
    const newErrors = {};
    
    Object.keys(validationRules).forEach(field => {
      const rule = validationRules[field];
      const value = values[field];
      
      if (rule.required && (!value || value.toString().trim() === '')) {
        newErrors[field] = rule.required;
      } else if (value && rule.pattern && !rule.pattern.test(value)) {
        newErrors[field] = rule.patternMessage || 'Invalid format';
      } else if (value && rule.minLength && value.length < rule.minLength) {
        newErrors[field] = `Minimum ${rule.minLength} characters required`;
      }
    });
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }, [values]);

  return {
    values,
    errors,
    touched,
    setValue,
    setTouched,
    setFieldError,
    reset,
    handleChange,
    handleBlur,
    validate,
    isValid: Object.keys(errors).length === 0,
    isDirty: Object.keys(touched).length > 0,
  };
}

このuseFormフックは、フォームの状態管理を簡単にします。

主な機能:

  • 値の管理:フォームの入力値を保存
  • エラー管理:バリデーションエラーを管理
  • タッチ状態:フィールドがタッチされたかを記録
  • バリデーション:入力値の検証
// 使用例
function ContactForm() {
  const form = useForm({
    name: '',
    email: '',
    message: ''
  });

  const validationRules = {
    name: {
      required: '名前は必須です',
      minLength: 2
    },
    email: {
      required: 'メールアドレスは必須です',
      pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
      patternMessage: '正しいメールアドレスを入力してください'
    },
    message: {
      required: 'メッセージは必須です',
      minLength: 10
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (form.validate(validationRules)) {
      console.log('Form data:', form.values);
      // API呼び出しなど
      form.reset();
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">名前</label>
        <input
          id="name"
          name="name"
          value={form.values.name}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
        />
        {form.errors.name && form.touched.name && (
          <span className="error">{form.errors.name}</span>
        )}
      </div>

      <div>
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          name="email"
          type="email"
          value={form.values.email}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
        />
        {form.errors.email && form.touched.email && (
          <span className="error">{form.errors.email}</span>
        )}
      </div>

      <div>
        <label htmlFor="message">メッセージ</label>
        <textarea
          id="message"
          name="message"
          value={form.values.message}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
        />
        {form.errors.message && form.touched.message && (
          <span className="error">{form.errors.message}</span>
        )}
      </div>

      <button type="submit" disabled={!form.isValid}>
        送信
      </button>
      
      <button type="button" onClick={form.reset}>
        リセット
      </button>
    </form>
  );
}

このフォームでは、以下の処理が自動的に行われます:

  • 入力値の管理
  • バリデーションの実行
  • エラーメッセージの表示
  • フォームのリセット

フォームを作るのが、とても簡単になりますね。

非同期処理パターン

API呼び出しなどの非同期処理を抽象化します。

// 基本的な非同期処理フック
function useAsync(asyncFunction, dependencies = []) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const execute = useCallback(async (...args) => {
    setLoading(true);
    setError(null);
    
    try {
      const result = await asyncFunction(...args);
      setData(result);
      return result;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [asyncFunction]);

  useEffect(() => {
    execute();
  }, dependencies);

  return {
    data,
    loading,
    error,
    execute,
    // 便利なプロパティ
    isSuccess: !loading && !error && data !== null,
    isError: !loading && error !== null,
  };
}

このuseAsyncフックは、非同期処理の共通パターンを提供します。

主な機能:

  • ローディング状態:処理中かどうかを管理
  • エラー状態:エラーが発生したかを管理
  • データ状態:取得したデータを管理
  • 再実行:処理を再度実行できる
// API呼び出し専用フック
function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async (requestOptions = {}) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url, {
        ...options,
        ...requestOptions,
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const result = await response.json();
      setData(result);
      return result;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  const post = useCallback(async (body) => {
    return fetchData({
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      body: JSON.stringify(body),
    });
  }, [fetchData, options.headers]);

  const put = useCallback(async (body) => {
    return fetchData({
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      body: JSON.stringify(body),
    });
  }, [fetchData, options.headers]);

  const del = useCallback(async () => {
    return fetchData({
      method: 'DELETE',
    });
  }, [fetchData]);

  return {
    data,
    loading,
    error,
    get: fetchData,
    post,
    put,
    delete: del,
    refetch: () => fetchData(),
  };
}

このuseApiフックは、API呼び出しを簡単にします。

主な機能:

  • GET:データを取得
  • POST:データを送信
  • PUT:データを更新
  • DELETE:データを削除
// 使用例
function UserProfile({ userId }) {
  const {
    data: user,
    loading,
    error,
    refetch
  } = useApi(`/api/users/${userId}`);

  const {
    post: updateUser,
    loading: updating,
    error: updateError
  } = useApi(`/api/users/${userId}`);

  const handleUpdate = async (userData) => {
    try {
      await updateUser(userData);
      refetch(); // データを再取得
    } catch (error) {
      console.error('Update failed:', error);
    }
  };

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
      
      <button onClick={refetch} disabled={loading}>
        Refresh
      </button>
      
      <UserEditForm 
        user={user} 
        onSubmit={handleUpdate}
        loading={updating}
        error={updateError}
      />
    </div>
  );
}

このようにAPI呼び出しが、とても簡単になります。

ローディング状態やエラーの管理も自動的に行われるので、コンポーネントはUIに集中できますね。

次は、さらに実践的なカスタムフックの例を見てみましょう。

実践的なカスタムフック例

実際のアプリケーション開発で使える実用的なカスタムフックを紹介します。

データフェッチングフック

高度なデータフェッチング機能を持つフックです。

// 高機能なデータフェッチングフック
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const abortControllerRef = useRef();

  const fetchData = useCallback(async (fetchOptions = {}) => {
    // 前回のリクエストをキャンセル
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    abortControllerRef.current = new AbortController();
    
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(url, {
        signal: abortControllerRef.current.signal,
        ...options,
        ...fetchOptions,
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = await response.json();
      setData(result);
      return result;
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError(err);
      }
      throw err;
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  // コンポーネントアンマウント時のクリーンアップ
  useEffect(() => {
    return () => {
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
    };
  }, []);

  return {
    data,
    loading,
    error,
    refetch: fetchData,
    // 便利なメソッド
    post: useCallback((body) => fetchData({
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }), [fetchData]),
    
    put: useCallback((body) => fetchData({
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }), [fetchData]),
    
    delete: useCallback(() => fetchData({
      method: 'DELETE',
    }), [fetchData]),
  };
}

このuseFetchフックは、先ほどの基本版よりも高機能です。

新しい機能:

  • リクエストキャンセル:前回のリクエストを自動的にキャンセル
  • クリーンアップ:コンポーネントが削除される時の処理
  • エラーハンドリング:キャンセルエラーを適切に処理

これにより、より安全で高性能なAPI呼び出しが可能になります。

// ページネーション付きデータフェッチング
function usePaginatedFetch(baseUrl, pageSize = 10) {
  const [page, setPage] = useState(1);
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [hasMore, setHasMore] = useState(true);
  const [total, setTotal] = useState(0);

  const fetchPage = useCallback(async (pageNumber) => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(
        `${baseUrl}?page=${pageNumber}&limit=${pageSize}`
      );
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = await response.json();
      
      setData(prev => pageNumber === 1 ? result.items : [...prev, ...result.items]);
      setTotal(result.total);
      setHasMore(result.items.length === pageSize);
      
      return result;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [baseUrl, pageSize]);

  const loadMore = useCallback(() => {
    if (!loading && hasMore) {
      const nextPage = page + 1;
      setPage(nextPage);
      fetchPage(nextPage);
    }
  }, [loading, hasMore, page, fetchPage]);

  const refresh = useCallback(() => {
    setPage(1);
    setData([]);
    fetchPage(1);
  }, [fetchPage]);

  // 初回読み込み
  useEffect(() => {
    fetchPage(1);
  }, [fetchPage]);

  return {
    data,
    loading,
    error,
    hasMore,
    total,
    page,
    loadMore,
    refresh,
    // 計算プロパティ
    isFirstPage: page === 1,
    totalPages: Math.ceil(total / pageSize),
    loadedCount: data.length,
  };
}

このusePaginatedFetchフックは、ページネーション機能付きです。

主な機能:

  • ページ管理:現在のページ番号を管理
  • データ蓄積:読み込んだデータを蓄積
  • 追加読み込み:「もっと読む」機能
  • リフレッシュ:最初から読み込み直し
// 使用例
function ProductList() {
  const {
    data: products,
    loading,
    error,
    hasMore,
    loadMore,
    refresh,
    loadedCount,
    total
  } = usePaginatedFetch('/api/products', 20);

  if (error) {
    return (
      <div>
        <p>Error: {error.message}</p>
        <button onClick={refresh}>Retry</button>
      </div>
    );
  }

  return (
    <div>
      <div className="header">
        <h1>Products ({loadedCount} of {total})</h1>
        <button onClick={refresh}>Refresh</button>
      </div>

      <div className="product-grid">
        {products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>

      {loading && <div>Loading...</div>}

      {hasMore && !loading && (
        <button onClick={loadMore}>Load More</button>
      )}

      {!hasMore && products.length > 0 && (
        <p>All products loaded!</p>
      )}
    </div>
  );
}

このコンポーネントでは、以下の機能が実現されています:

  • 商品の一覧表示
  • 追加読み込み(Load More)
  • リフレッシュ機能
  • 読み込み状態の表示

とても便利な機能ですね。

認証管理フック

ユーザー認証状態を管理するフックです。

// 認証管理フック
function useAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // トークンの検証と初期化
  useEffect(() => {
    const initAuth = async () => {
      try {
        const token = localStorage.getItem('authToken');
        if (token) {
          const response = await fetch('/api/auth/verify', {
            headers: { Authorization: `Bearer ${token}` },
          });
          
          if (response.ok) {
            const userData = await response.json();
            setUser(userData);
          } else {
            localStorage.removeItem('authToken');
          }
        }
      } catch (err) {
        console.error('Auth initialization failed:', err);
        localStorage.removeItem('authToken');
      } finally {
        setLoading(false);
      }
    };

    initAuth();
  }, []);

  // ログイン
  const login = useCallback(async (credentials) => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });

      if (!response.ok) {
        throw new Error('Login failed');
      }

      const { user: userData, token } = await response.json();
      
      localStorage.setItem('authToken', token);
      setUser(userData);
      
      return userData;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  // ログアウト
  const logout = useCallback(async () => {
    setLoading(true);

    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        headers: { 
          Authorization: `Bearer ${localStorage.getItem('authToken')}` 
        },
      });
    } catch (err) {
      console.error('Logout error:', err);
    } finally {
      localStorage.removeItem('authToken');
      setUser(null);
      setLoading(false);
    }
  }, []);

  // パスワード変更
  const changePassword = useCallback(async (currentPassword, newPassword) => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/auth/change-password', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${localStorage.getItem('authToken')}`,
        },
        body: JSON.stringify({ currentPassword, newPassword }),
      });

      if (!response.ok) {
        throw new Error('Password change failed');
      }

      return true;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  return {
    user,
    loading,
    error,
    login,
    logout,
    changePassword,
    // 便利なプロパティ
    isAuthenticated: !!user,
    isAdmin: user?.role === 'admin',
    userName: user?.name,
    userEmail: user?.email,
  };
}

このuseAuthフックは、ユーザー認証を管理します。

主な機能:

  • 自動ログイン:ページリロード時の認証状態復元
  • ログイン処理:認証情報の送信と保存
  • ログアウト処理:認証情報の削除
  • パスワード変更:パスワード更新機能
// 権限チェックフック
function usePermissions(requiredPermissions = []) {
  const { user, isAuthenticated } = useAuth();

  const hasPermission = useCallback((permission) => {
    if (!isAuthenticated || !user) return false;
    return user.permissions?.includes(permission) || user.role === 'admin';
  }, [isAuthenticated, user]);

  const hasAllPermissions = useCallback(() => {
    return requiredPermissions.every(permission => hasPermission(permission));
  }, [requiredPermissions, hasPermission]);

  const hasAnyPermission = useCallback(() => {
    return requiredPermissions.some(permission => hasPermission(permission));
  }, [requiredPermissions, hasPermission]);

  return {
    hasPermission,
    hasAllPermissions,
    hasAnyPermission,
    canAccess: hasAllPermissions(),
    permissions: user?.permissions || [],
    role: user?.role,
  };
}

このusePermissionsフックは、権限チェックを簡単にします。

主な機能:

  • 権限チェック:特定の権限を持っているかチェック
  • 複数権限:すべての権限を持っているかチェック
  • アクセス可能性:画面にアクセスできるかチェック
// 使用例
function ProtectedComponent() {
  const { user, isAuthenticated, logout } = useAuth();
  const { canAccess } = usePermissions(['read:posts', 'write:posts']);

  if (!isAuthenticated) {
    return <LoginForm />;
  }

  if (!canAccess) {
    return <div>You don't have permission to access this resource.</div>;
  }

  return (
    <div>
      <header>
        <span>Welcome, {user.name}!</span>
        <button onClick={logout}>Logout</button>
      </header>
      
      <main>
        <PostList />
        <PostEditor />
      </main>
    </div>
  );
}

このコンポーネントでは、以下の処理が行われています:

  • 認証状態のチェック
  • 権限のチェック
  • 適切な画面の表示

認証と権限管理が、とても簡単に実装できますね。

リアルタイムデータフック

WebSocketを使ったリアルタイムデータ管理です。

// WebSocketフック
function useWebSocket(url, options = {}) {
  const [socket, setSocket] = useState(null);
  const [lastMessage, setLastMessage] = useState(null);
  const [readyState, setReadyState] = useState(WebSocket.CONNECTING);
  const [error, setError] = useState(null);

  const reconnectTimeoutRef = useRef();
  const reconnectAttemptsRef = useRef(0);
  const maxReconnectAttempts = options.maxReconnectAttempts || 5;
  const reconnectInterval = options.reconnectInterval || 3000;

  const connect = useCallback(() => {
    try {
      const ws = new WebSocket(url);
      
      ws.onopen = () => {
        setReadyState(WebSocket.OPEN);
        setError(null);
        reconnectAttemptsRef.current = 0;
        console.log('WebSocket connected');
      };

      ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
        setLastMessage(message);
        options.onMessage?.(message);
      };

      ws.onclose = (event) => {
        setReadyState(WebSocket.CLOSED);
        setSocket(null);
        
        if (!event.wasClean && reconnectAttemptsRef.current < maxReconnectAttempts) {
          reconnectTimeoutRef.current = setTimeout(() => {
            reconnectAttemptsRef.current++;
            console.log(`Reconnecting... (${reconnectAttemptsRef.current}/${maxReconnectAttempts})`);
            connect();
          }, reconnectInterval);
        }
      };

      ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        setError(error);
      };

      setSocket(ws);
    } catch (err) {
      setError(err);
    }
  }, [url, options, maxReconnectAttempts, reconnectInterval]);

  const disconnect = useCallback(() => {
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current);
    }
    
    if (socket) {
      socket.close();
    }
  }, [socket]);

  const sendMessage = useCallback((message) => {
    if (socket && readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify(message));
      return true;
    }
    return false;
  }, [socket, readyState]);

  useEffect(() => {
    connect();
    
    return () => {
      disconnect();
    };
  }, [connect, disconnect]);

  return {
    socket,
    lastMessage,
    readyState,
    error,
    sendMessage,
    disconnect,
    reconnect: connect,
    // 便利なプロパティ
    isConnecting: readyState === WebSocket.CONNECTING,
    isOpen: readyState === WebSocket.OPEN,
    isClosing: readyState === WebSocket.CLOSING,
    isClosed: readyState === WebSocket.CLOSED,
  };
}

このuseWebSocketフックは、WebSocket接続を管理します。

主な機能:

  • 自動接続:コンポーネントマウント時の自動接続
  • 自動再接続:接続が切れた時の自動再接続
  • メッセージ送信:JSONメッセージの送信
  • 状態管理:接続状態の管理
// リアルタイムチャットフック
function useChat(roomId) {
  const [messages, setMessages] = useState([]);
  const [users, setUsers] = useState([]);
  const [typing, setTyping] = useState([]);

  const { sendMessage, lastMessage, isOpen } = useWebSocket(
    `ws://localhost:8080/chat/${roomId}`,
    {
      onMessage: (message) => {
        switch (message.type) {
          case 'message':
            setMessages(prev => [...prev, message.data]);
            break;
          case 'user_joined':
            setUsers(prev => [...prev, message.data]);
            break;
          case 'user_left':
            setUsers(prev => prev.filter(user => user.id !== message.data.id));
            break;
          case 'typing_start':
            setTyping(prev => [...prev, message.data.userId]);
            break;
          case 'typing_stop':
            setTyping(prev => prev.filter(id => id !== message.data.userId));
            break;
        }
      }
    }
  );

  const sendChatMessage = useCallback((text) => {
    if (isOpen) {
      sendMessage({
        type: 'message',
        data: { text, timestamp: Date.now() }
      });
    }
  }, [sendMessage, isOpen]);

  const startTyping = useCallback(() => {
    if (isOpen) {
      sendMessage({ type: 'typing_start' });
    }
  }, [sendMessage, isOpen]);

  const stopTyping = useCallback(() => {
    if (isOpen) {
      sendMessage({ type: 'typing_stop' });
    }
  }, [sendMessage, isOpen]);

  return {
    messages,
    users,
    typing,
    sendMessage: sendChatMessage,
    startTyping,
    stopTyping,
    isConnected: isOpen,
  };
}

このuseChatフックは、チャット機能を提供します。

主な機能:

  • メッセージ管理:チャットメッセージの管理
  • ユーザー管理:チャットルームのユーザー管理
  • タイピング表示:誰がタイピング中かを表示
  • リアルタイム更新:すべてリアルタイムで更新
// 使用例
function ChatRoom({ roomId }) {
  const {
    messages,
    users,
    typing,
    sendMessage,
    startTyping,
    stopTyping,
    isConnected
  } = useChat(roomId);

  const [inputValue, setInputValue] = useState('');
  const typingTimeoutRef = useRef();

  const handleInputChange = (e) => {
    setInputValue(e.target.value);
    
    // タイピング開始
    startTyping();
    
    // タイピング停止のタイマー
    clearTimeout(typingTimeoutRef.current);
    typingTimeoutRef.current = setTimeout(() => {
      stopTyping();
    }, 1000);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      sendMessage(inputValue);
      setInputValue('');
      stopTyping();
    }
  };

  return (
    <div className="chat-room">
      <div className="chat-header">
        <h2>Room: {roomId}</h2>
        <div className="connection-status">
          {isConnected ? '🟢 Connected' : '🔴 Disconnected'}
        </div>
      </div>

      <div className="users-list">
        <h3>Users ({users.length})</h3>
        <ul>
          {users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      </div>

      <div className="messages">
        {messages.map((message, index) => (
          <div key={index} className="message">
            <strong>{message.sender}:</strong> {message.text}
            <span className="timestamp">
              {new Date(message.timestamp).toLocaleTimeString()}
            </span>
          </div>
        ))}
        
        {typing.length > 0 && (
          <div className="typing-indicator">
            {typing.join(', ')} typing...
          </div>
        )}
      </div>

      <form onSubmit={handleSubmit} className="message-form">
        <input
          type="text"
          value={inputValue}
          onChange={handleInputChange}
          placeholder="Type a message..."
          disabled={!isConnected}
        />
        <button type="submit" disabled={!isConnected || !inputValue.trim()}>
          Send
        </button>
      </form>
    </div>
  );
}

このチャットルームでは、以下の機能が実現されています:

  • リアルタイムメッセージ送受信
  • ユーザーのオンライン状態表示
  • タイピング中の表示
  • 接続状態の表示

とても高機能なチャットアプリが作れますね。

次は、カスタムフックのテストとデバッグ方法を見てみましょう。

テストとデバッグ

カスタムフックのテストとデバッグ方法を紹介します。

カスタムフックのテスト

React Testing Libraryを使ったテスト方法です。

// テスト対象のカスタムフック
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  const decrement = useCallback(() => {
    setCount(prev => prev - 1);
  }, []);

  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

  return { count, increment, decrement, reset };
}

このuseCounterフックのテストを書いてみましょう。

// テストコード
import { renderHook, act } from '@testing-library/react';

describe('useCounter', () => {
  test('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  test('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  test('should increment count', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  test('should decrement count', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });

  test('should reset to initial value', () => {
    const { result } = renderHook(() => useCounter(3));
    
    act(() => {
      result.current.increment();
      result.current.increment();
    });
    
    expect(result.current.count).toBe(5);
    
    act(() => {
      result.current.reset();
    });
    
    expect(result.current.count).toBe(3);
  });

  test('should update when initial value changes', () => {
    let initialValue = 0;
    const { result, rerender } = renderHook(() => useCounter(initialValue));
    
    expect(result.current.count).toBe(0);
    
    initialValue = 10;
    rerender();
    
    act(() => {
      result.current.reset();
    });
    
    expect(result.current.count).toBe(10);
  });
});

テストのポイント:

  • renderHookでカスタムフックを実行
  • actで状態更新をラップ
  • result.currentで現在の値にアクセス
  • rerenderで再レンダリングをシミュレート

カスタムフックのテストは、コンポーネントのテストよりもシンプルですね。

// 非同期フックのテスト
describe('useAsync', () => {
  const mockAsyncFunction = jest.fn();

  beforeEach(() => {
    mockAsyncFunction.mockClear();
  });

  test('should handle successful async operation', async () => {
    const mockData = { id: 1, name: 'Test' };
    mockAsyncFunction.mockResolvedValue(mockData);

    const { result, waitForNextUpdate } = renderHook(() => 
      useAsync(mockAsyncFunction)
    );

    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBe(null);
    expect(result.current.error).toBe(null);

    await waitForNextUpdate();

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBe(null);
    expect(mockAsyncFunction).toHaveBeenCalledTimes(1);
  });

  test('should handle async operation error', async () => {
    const mockError = new Error('Test error');
    mockAsyncFunction.mockRejectedValue(mockError);

    const { result, waitForNextUpdate } = renderHook(() => 
      useAsync(mockAsyncFunction)
    );

    expect(result.current.loading).toBe(true);

    await waitForNextUpdate();

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toBe(null);
    expect(result.current.error).toBe(mockError);
  });

  test('should re-execute when dependencies change', async () => {
    let dependency = 'initial';
    mockAsyncFunction.mockResolvedValue('result');

    const { rerender, waitForNextUpdate } = renderHook(() => 
      useAsync(mockAsyncFunction, [dependency])
    );

    await waitForNextUpdate();
    expect(mockAsyncFunction).toHaveBeenCalledTimes(1);

    dependency = 'changed';
    rerender();
    
    await waitForNextUpdate();
    expect(mockAsyncFunction).toHaveBeenCalledTimes(2);
  });
});

非同期テストのポイント:

  • waitForNextUpdateで非同期処理の完了を待機
  • mockResolvedValueで成功のシミュレート
  • mockRejectedValueでエラーのシミュレート
  • 依存配列の変更による再実行のテスト

非同期フックのテストも、このパターンを覚えれば簡単にできますね。

モックとスタブ

外部依存をモックする方法です。

// fetchをモックしたAPIフックのテスト
global.fetch = jest.fn();

describe('useApi', () => {
  beforeEach(() => {
    fetch.mockClear();
  });

  test('should fetch data successfully', async () => {
    const mockData = { users: [{ id: 1, name: 'John' }] };
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockData,
    });

    const { result, waitForNextUpdate } = renderHook(() => 
      useApi('/api/users')
    );

    expect(result.current.loading).toBe(false);

    act(() => {
      result.current.get();
    });

    expect(result.current.loading).toBe(true);
    
    await waitForNextUpdate();

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBe(null);
    expect(fetch).toHaveBeenCalledWith('/api/users', {});
  });

  test('should handle fetch error', async () => {
    fetch.mockResolvedValueOnce({
      ok: false,
      status: 500,
    });

    const { result, waitForNextUpdate } = renderHook(() => 
      useApi('/api/users')
    );

    act(() => {
      result.current.get();
    });

    await waitForNextUpdate();

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toBe(null);
    expect(result.current.error).toBeInstanceOf(Error);
    expect(result.current.error.message).toBe('HTTP error! status: 500');
  });
});

APIテストのポイント:

  • global.fetchをモック関数に置き換え
  • mockResolvedValueOnceでレスポンスのシミュレート
  • HTTPエラーのテスト
  • 正常なレスポンスとエラーレスポンスの両方をテスト
// localStorageをモックしたテスト
const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn(),
};

global.localStorage = localStorageMock;

describe('useLocalStorage', () => {
  beforeEach(() => {
    localStorageMock.getItem.mockClear();
    localStorageMock.setItem.mockClear();
    localStorageMock.removeItem.mockClear();
  });

  test('should return initial value when localStorage is empty', () => {
    localStorageMock.getItem.mockReturnValue(null);

    const { result } = renderHook(() => 
      useLocalStorage('test-key', 'default-value')
    );

    expect(result.current[0]).toBe('default-value');
    expect(localStorageMock.getItem).toHaveBeenCalledWith('test-key');
  });

  test('should return stored value from localStorage', () => {
    localStorageMock.getItem.mockReturnValue('"stored-value"');

    const { result } = renderHook(() => 
      useLocalStorage('test-key', 'default-value')
    );

    expect(result.current[0]).toBe('stored-value');
  });

  test('should update localStorage when value changes', () => {
    localStorageMock.getItem.mockReturnValue(null);

    const { result } = renderHook(() => 
      useLocalStorage('test-key', 'default-value')
    );

    act(() => {
      result.current[1]('new-value');
    });

    expect(result.current[0]).toBe('new-value');
    expect(localStorageMock.setItem).toHaveBeenCalledWith(
      'test-key', 
      '"new-value"'
    );
  });
});

ローカルストレージテストのポイント:

  • localStorageをモックオブジェクトに置き換え
  • getItemsetItemの動作をシミュレート
  • 初期値と保存値の両方をテスト
  • JSON文字列の変換も確認

デバッグ用フック

カスタムフックのデバッグに役立つツールです。

// デバッグ用フック
function useDebugValue(value, formatter) {
  useDebugValue(value, formatter);
}

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();
  
  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};
      
      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });
      
      if (Object.keys(changedProps).length) {
        console.log('[Why-Did-You-Update]', name, changedProps);
      }
    }
    
    previousProps.current = props;
  });
}

このuseWhyDidYouUpdateフックは、再レンダリングの原因を調べます。

どのpropsが変更されたかをコンソールに出力してくれるので、とても便利です。

// 使用例
function useCounterWithDebug(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  // React DevToolsでの表示用
  useDebugValue(count, count => `Count: ${count}`);

  // 再レンダリングの原因を追跡
  useWhyDidYouUpdate('useCounter', { initialValue });

  const increment = useCallback(() => {
    console.log('Counter incremented');
    setCount(prev => prev + 1);
  }, []);

  const decrement = useCallback(() => {
    console.log('Counter decremented');
    setCount(prev => prev - 1);
  }, []);

  const reset = useCallback(() => {
    console.log('Counter reset');
    setCount(initialValue);
  }, [initialValue]);

  return { count, increment, decrement, reset };
}

デバッグフックのポイント:

  • useDebugValueでReact DevToolsに情報を表示
  • useWhyDidYouUpdateで再レンダリングの原因を追跡
  • コンソールログで動作を確認

これらのツールを使うことで、カスタムフックの動作をより詳しく理解できますね。

次は、カスタムフックの活用についてまとめてみましょう。

まとめ

カスタムフックを活用することで、Reactアプリケーションが劇的に改善されます。

主要なメリット

カスタムフック導入によって得られる主なメリットをまとめます。

ロジックの再利用 同じ機能を複数のコンポーネントで使用できます。 APIコールやフォーム管理など、よく使う機能をまとめられます。

関心の分離 UIロジックとビジネスロジックを分離できます。 コンポーネントは表示に集中し、データ管理はフックが担当します。

テストの容易さ ロジックを独立してテストできます。 バグの発見が早くなり、コードの品質が向上します。

保守性の向上 変更箇所を一箇所に集約できます。 機能の修正や拡張が簡単になります。

可読性の向上 コンポーネントがシンプルになります。 コードの理解が格段に楽になります。

作成時のベストプラクティス

効果的なカスタムフックを作成するためのポイントです。

// ✅ 良いカスタムフック
function useUserData(userId) {
  // 1. 明確な命名(use + 機能名)
  // 2. 適切な依存配列の管理
  // 3. エラーハンドリング
  // 4. クリーンアップ処理
  // 5. 型定義(TypeScript使用時)
  
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        });
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    if (userId) {
      fetchUser();
    }

    return () => controller.abort();
  }, [userId]);

  return { user, loading, error };
}

重要なポイント:

  • 明確な命名:関数名から機能がわかるように
  • エラーハンドリング:予期しないエラーを適切に処理
  • クリーンアップ:メモリリークを防ぐ
  • 依存配列:無限ループを避ける
  • 型定義:TypeScriptを使う場合は型を定義

活用パターンの使い分け

状況に応じた適切なパターンの選択指針です。

// 状態管理パターン
// 単純な状態の管理と操作
const useToggle = (initial) => { /* ... */ };
const useCounter = (initial) => { /* ... */ };
const useLocalStorage = (key, initial) => { /* ... */ };

// データフェッチングパターン
// API呼び出しとその結果の管理
const useApi = (url) => { /* ... */ };
const usePaginatedData = (endpoint) => { /* ... */ };
const useRealTimeData = (socketUrl) => { /* ... */ };

// フォーム管理パターン
// フォームの状態とバリデーション
const useForm = (initialValues) => { /* ... */ };
const useFieldValidation = (rules) => { /* ... */ };

// 副作用パターン
// DOM操作やイベント監視
const useEventListener = (event, handler) => { /* ... */ };
const useIntersectionObserver = (options) => { /* ... */ };
const useDebounce = (value, delay) => { /* ... */ };

パターンの選び方:

  • シンプルな状態管理:基本的な状態管理パターン
  • API呼び出し:データフェッチングパターン
  • フォーム:フォーム管理パターン
  • DOM操作:副作用パターン

今後の学習

カスタムフックをマスターするための学習ステップです。

// 1. 基本パターンの習得
// - useState、useEffectの組み合わせ
// - 依存配列の理解
// - クリーンアップの実装

// 2. 実践的なフックの作成
// - API呼び出し
// - フォーム管理
// - ローカルストレージ連携

// 3. 高度なテクニック
// - パフォーマンス最適化
// - エラーハンドリング
// - TypeScript対応

// 4. ライブラリの活用
// - React Query / SWR
// - Zustand / Recoil
// - React Hook Form

学習のステップ:

  1. 基本を固める:useStateとuseEffectの理解
  2. 実践で試す:簡単なフックから始める
  3. 応用する:複雑なフックにチャレンジ
  4. ライブラリを活用:既存の優秀なライブラリを学ぶ

最後に

カスタムフックは、React開発において非常に強力なツールです。 適切に活用することで、コードの品質と開発効率を大幅に向上させることができます。

まずは簡単なカスタムフックから始めてみましょう。

  • useToggleでオン・オフの切り替え
  • useCounterでカウンター機能
  • useLocalStorageでデータの保存

これらを作って動かしてみるだけでも、カスタムフックの便利さを実感できるはずです。

慣れてきたら、API呼び出しやフォーム管理などの実践的なフックにチャレンジしてください。 継続的に学習と実践を重ねることで、より効率的なReact開発が可能になります。

ぜひ、今日から始めてみてくださいね!

関連記事