0Pricing
Frontend Academy · レッスン

カスタム Hook:再利用可能なロジックの抽出

状態を持つロジックをカスタム use* 関数に移し、コードの重複や状態のリフトアップなしにコンポーネント間で共有します。

「カスタム Hook:再利用可能なロジックの抽出」はCoddyKit上の無料Frontend Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはFrontend Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Frontend Academyコースには全4レッスンが含まれています。

カスタムフックとは

カスタムフックは、名前がuseで始まり、内部で他のフックを呼び出すJavaScript関数です。カスタムフックを使うと、状態を持つロジックを取り出して、コピー&ペーストせずに複数のコンポーネントで共有できます。

ロジックをカスタムフックに切り出す

コンポーネント間で重複しているロジック(fetch + loading + error、ウィンドウサイズ、ローカルストレージなど)を見つけ、カスタムフックに移します。

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);

  return width;
}

// Usage:
const width = useWindowWidth();
const isMobile = width < 768;

useFetch — データ取得フック

どのコンポーネントからでもURLのデータを読み込める、再利用可能なデータ取得フックです。

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    async function load() {
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`${res.status}`);
        setData(await res.json());
      } catch (err) {
        if ((err as DOMException).name !== 'AbortError')
          setError((err as Error).message);
      } finally { setLoading(false); }
    }
    load();
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

useLocalStorageフック

値をlocalStorageと同期し、storageイベントによってタブ間でも同期状態を保つカスタムフックです。

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  const set = useCallback((newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  }, [key]);

  return [value, set] as const;
}

カスタムフックはロジックを共有し、状態は共有しない

カスタムフックを呼び出す各コンポーネントは、それぞれ独立した状態を持ちます。フックはシングルトンではなく、分離された状態のインスタンスを作成する関数呼び出しです。

命名規則

カスタムフックの名前はuseで始める必要があります。これはESLintのreact-hooksリンティングルールによって強制されます。このルールにより、フックがトップレベルで、正しい順序で呼び出されることが保証されます。

複数の値を返す

カスタムフックは通常、オブジェクト(名前付きの値)またはタプル(useStateのようなもの)を返します。タプルでは任意の名前で分割代入できますが、オブジェクトは値の意味がコードから分かりやすくなります。

// Tuple (like useState):
const [value, setValue] = useLocalStorage('key', 0);

// Object (more descriptive):
const { data, loading, error, refetch } = useFetch('/api/users');

useDebounceフック

値をデバウンスします。ユーザーが指定した時間入力を止めた後にだけ値を更新します。

function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

// Usage:
const debouncedSearch = useDebounce(searchTerm, 300);
useEffect(() => { search(debouncedSearch); }, [debouncedSearch]);

useMediaQueryフック

window.matchMediaをフックでラップし、CSSメディアクエリの結果をリアクティブに読み取ります。

function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches);

  useEffect(() => {
    const mq = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

カスタムフックのテスト

@testing-library/reactのrenderHook()ユーティリティを使うと、コンポーネントでラップせずにカスタムフックを単独でテストできます。

import { renderHook, act } from '@testing-library/react';

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

公開されているフックライブラリ

コミュニティ製のフックライブラリには、テスト済みのフックが数百種類用意されています: @uidotdev/usehooks、react-use、usehooks-ts。カスタムフックを書く前に、十分にテストされた実装がすでに存在しないか確認してください。

理解度チェック

カスタムフックが2つの異なるコンポーネントから呼び出された場合、フック内部の状態はどうなりますか?

復習: カスタムフック

カスタムフックは、再利用するために状態を持つロジックを切り出します。名前にはuseプレフィックスを付けます。フックを呼び出す各コンポーネントは独立した状態を持ちます。オブジェクトまたはタプルを返します。よくあるパターンには、useFetch、useLocalStorage、useDebounce、useMediaQueryがあります。renderHook()でテストします。一から実装する前に、コミュニティ製ライブラリを確認してください。

よくある質問

「カスタム Hook:再利用可能なロジックの抽出」レッスンは無料ですか?

はい。「カスタム Hook:再利用可能なロジックの抽出」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Frontend Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Frontend Academyコースには全4レッスンが含まれています。

「カスタム Hook:再利用可能なロジックの抽出」で何を学びますか?

状態を持つロジックをカスタム use* 関数に移し、コードの重複や状態のリフトアップなしにコンポーネント間で共有します。 ブラウザで直接実行するハンズオンコードでFrontend Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Frontend Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのFrontend Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「カスタム Hook:再利用可能なロジックの抽出」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このFrontend Academyレッスンでコードを書いて実行できますか?

はい。すべてのFrontend Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. グローバル状態のための useContext
  2. 複雑な状態のための useReducer
  3. パフォーマンスのための useMemo と useCallback
  4. カスタム Hook:再利用可能なロジックの抽出
← Frontend Academyに戻る