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しよう!【Part23: カスタムフック基礎編】

Last updated at Posted at 2025-12-25

React を使っていると、「このロジック、他のコンポーネントでも使いたいな...」と思うことがあります。そんなとき、既存の Hooks を組み合わせて独自のロジックを作成できるのが**カスタムフック(Custom Hooks)**です。本記事では、カスタムフックの基礎と内部構造を解説します。

検証環境: この記事は facebook/react リポジトリ(2025年12月時点の main ブランチ)の実際のソースコードを参照して書かれています。

1. なぜカスタムフックが必要か

1.1 ロジックの重複問題

複数のコンポーネントで同じロジックを使う場合、従来は以下のような問題がありました:

// ❌ ロジックが重複
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connect', () => setIsConnected(true));
    connection.on('message', (msg) => setMessages(prev => [...prev, msg]));
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <div>...</div>;
}

function AnotherChatRoom({ roomId }) {
  // 同じロジックをまた書く必要がある...
  const [messages, setMessages] = useState([]);
  // ...
}

問題点:

  • 同じロジックが複数のコンポーネントに散在
  • バグ修正や機能追加の際に複数箇所を修正する必要がある
  • テストが困難

1.2 カスタムフックによる解決

カスタムフックを使うと、ロジックを1箇所に集約し、複数のコンポーネントで再利用できます:

// ✅ カスタムフックに抽出
function useChatRoom(roomId: string) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connect', () => setIsConnected(true));
    connection.on('message', (msg) => setMessages(prev => [...prev, msg]));
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return { messages, isConnected };
}

// 使用側
function ChatRoom({ roomId }) {
  const { messages, isConnected } = useChatRoom(roomId);
  return <div>...</div>;
}

1.3 カスタムフックの定義

カスタムフックは、use で始まる関数です。React の組み込み Hooks を呼び出すことができます。

function useCustomHook(initialValue: number) {
  const [value, setValue] = useState(initialValue);

  useEffect(() => {
    // 何らかの副作用
  }, [value]);

  return { value, setValue };
}

重要なルール:

  1. 関数名は use で始まる必要がある(規約)
  2. 他の Hooks を呼び出すことができる
  3. 任意の値を返すことができる

💡 use で始まらない関数では Hooks を使えない?

技術的には、use で始まらない関数でも React の組み込み Hooks(useStateuseEffect など)を呼び出すことは可能です。しかし、ESLint の react-hooks/rules-of-hooks ルールが警告を出します。

// ❌ ESLint が警告を出す(技術的には動作する)
function customHook(initialValue: number) {
  const [value, setValue] = useState(initialValue); // 警告: React Hook "useState" is called in function "customHook" that is neither a React function component nor a custom React Hook function
  return { value, setValue };
}

// ✅ 推奨される書き方
function useCustomHook(initialValue: number) {
  const [value, setValue] = useState(initialValue);
  return { value, setValue };
}

なぜ use で始める必要があるのか?

  • ESLint が Hooks のルール違反を検出できる
  • コードの可読性が向上する(カスタムフックであることが明確)
  • React DevTools でカスタムフックを識別できる

1.4 カスタムフックの利点

利点 説明
ロジックの再利用 複数のコンポーネントで同じロジックを共有
関心の分離 UI ロジックとビジネスロジックを分離
テスト容易性 カスタムフックを単独でテスト可能
可読性の向上 コンポーネントがシンプルになり、意図が明確に

2. カスタムフックの内部構造

カスタムフックは、実は特別な実装はありません。通常の関数として実装され、React の Hooks ルールに従って動作します。

2.1 全体像

📦 カスタムフック関数(useXXX)
   ↓
🎣 内部で React Hooks を呼び出す
   ↓
🔗 Hook チェーンに追加される
   ↓
📤 値を返す(任意の型)

カスタムフックは、呼び出し元のコンポーネントのHook チェーンに直接追加されます。

2.2 実行コンテキスト

カスタムフックが呼ばれると、React はそれを呼び出し元のコンポーネントの一部として扱います:

function MyComponent() {
  const [count, setCount] = useState(0);  // Hook #1
  const { value } = useCustomHook(10);   // Hook #2, #3 (useState, useEffect)
  const [name, setName] = useState('');   // Hook #4
  return <div>...</div>;
}

内部的な処理:

// packages/react-reconciler/src/ReactFiberHooks.js

function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: Function,
  props: any,
  ...
) {
  // Dispatcher を設定
  ReactSharedInternals.H = /* マウント or 更新 */;

  // コンポーネントを実行(この中でカスタムフックも呼ばれる)
  let children = Component(props);

  // Dispatcher をリセット
  ReactSharedInternals.H = ContextOnlyDispatcher;

  return children;
}

カスタムフック内で呼ばれる useStateuseEffect は、呼び出し元のコンポーネントの Hook チェーンに追加されます。

2.3 Hook チェーンへの追加

function useCounter(initialValue: number) {
  const [count, setCount] = useState(initialValue);  // ← Hook チェーンに追加
  return { count, setCount };
}

function MyComponent() {
  const [name, setName] = useState('');      // Hook #1
  const { count, setCount } = useCounter(0); // Hook #2 (useCounter 内の useState)
  const [age, setAge] = useState(0);         // Hook #3
}

内部的な Hook チェーン構造:

Fiber.memoizedState
       ↓
    Hook1 (useState: name)
       ↓
    Hook2 (useState: count) ← useCounter 内の useState
       ↓
    Hook3 (useState: age)
       ↓
    null

2.4 カスタムフックのルール

カスタムフックも、通常の Hooks と同じルールに従う必要があります:

ルール1: トップレベルでのみ呼び出す

// ❌ 条件分岐内で呼び出せない
function MyComponent({ flag }) {
  if (flag) {
    const { value } = useCustomHook(); // エラー!
  }
}

// ✅ トップレベルで呼び出す
function MyComponent({ flag }) {
  const { value } = useCustomHook();
  if (flag) {
    // value を使う
  }
}

ルール2: ループ内で呼び出せない

// ❌ ループ内で呼び出せない
function MyComponent({ items }) {
  return items.map(item => {
    const { value } = useCustomHook(item); // エラー!
    return <div>{value}</div>;
  });
}

// ✅ コンポーネントを分離
function Item({ item }) {
  const { value } = useCustomHook(item);
  return <div>{value}</div>;
}

2.5 カスタムフックの識別

React は、カスタムフックを特別には扱いませんuse で始まる関数名は、開発者とツール(ESLint など)のための規約です。

// packages/eslint-plugin-react-hooks/src/RulesOfHooks.js

function isHookName(name: string): boolean {
  return /^use[A-Z0-9]/.test(name);
}

ESLint の react-hooks/rules-of-hooks ルールは、use で始まる関数内でのみ Hooks を呼び出すことを強制します。

2.6 実行フロー

3. 代表的なカスタムフックパターン

3.1 状態管理フック

状態とその更新ロジックをカプセル化します。

function useBoolean(initialValue: boolean = false) {
  const [value, setValue] = useState(initialValue);

  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  const toggle = useCallback(() => setValue(v => !v), []);

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

// 使用例
function Modal() {
  const { value: isOpen, setTrue: open, setFalse: close } = useBoolean();

  return (
    <>
      <button onClick={open}>開く</button>
      {isOpen && (
        <div>
          <p>モーダル</p>
          <button onClick={close}>閉じる</button>
        </div>
      )}
    </>
  );
}

3.2 副作用フック

外部システムとの同期をカプセル化します。

function useWindowSize() {
  const [size, setSize] = useState({
    width: typeof window !== 'undefined' ? window.innerWidth : 0,
    height: typeof window !== 'undefined' ? window.innerHeight : 0,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

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

API からのデータ取得をカプセル化します。

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

  useEffect(() => {
    let ignore = false;

    setLoading(true);
    setError(null);

    fetch(url)
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .then(json => {
        if (!ignore) {
          setData(json);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!ignore) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      ignore = true;
    };
  }, [url]);

  return { data, loading, error };
}

3.4 前回値の保持フック

前回のレンダー時の値を保持します。

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

3.5 デバウンスフック

ユーザー入力の最適化に使用します。

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

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

3.6 ローカルストレージ同期フック

ローカルストレージと状態を同期します。

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = useCallback((value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue] as const;
}

4. まとめ

  1. カスタムフックは use で始まる関数
  2. 内部で React Hooks を呼び出すことができる
  3. 呼び出し元のコンポーネントの Hook チェーンに追加される
  4. Hooks のルール(トップレベル、条件分岐禁止)に従う必要がある
  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?