React Custom Hook設計でロジック分離を失敗しないための実務ポイント

React Custom Hook設計でUIとロジックを分離する構成図

Custom Hookは重複排除だけで作らない

React Custom Hook 設計で失敗しやすいのは、似たコードを見つけた瞬間にHookへ切り出してしまうことです。

結論から言うと、Custom Hookは重複排除の道具だけではありません。状態、イベント、副作用、外部APIとの同期を、UIから分離して名前を付けるための設計単位です。

ただし、切り出した結果、引数が増え、戻り値が肥大化し、useEffectの依存が読めなくなるなら逆効果です。そのため、実務では「何を隠し、何を公開するか」を先に決めます。

React Custom Hook設計で切り出す判断基準

切り出す対象向いている度合い理由
API取得と読み込み状態高い状態と副作用がまとまりやすい
フォーム入力の補助高い変更処理と検証をまとめやすい
単純な表示条件低いコンポーネント内で読める方がよい
複数画面で使う業務ロジック高いテスト対象として切り出しやすい
一度しか使わない巨大処理中程度名前で責務が明確になるなら有効

例えば、React公式のReusing Logic with Custom Hooksでも、Custom Hookはロジックを再利用する手段として説明されています。ただし、実務では再利用よりも責務分離が効く場面も多いです。

失敗例:何でも返すuseUserは読みにくい

まず、次のようなHookは最初は便利です。しかし、画面が増えるほど責務が膨らみます。

function useUser(userId: string) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [editing, setEditing] = useState(false);
  const [message, setMessage] = useState("");

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);

  return { user, loading, editing, setEditing, message, setMessage };
}

つまり、取得、編集状態、メッセージが混ざっています。その結果、このHookを修正するとき、どの画面に影響するか読みづらくなります。

改善例:責務でHookを分ける

function useUserQuery(userId: string) {
  const [state, setState] = useState<{
    user: User | null;
    loading: boolean;
  }>({ user: null, loading: false });

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

    setState((current) => ({ ...current, loading: true }));
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((user) => {
        if (!ignore) setState({ user, loading: false });
      });

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

  return state;
}

一方で、このHookはユーザー取得に集中しています。そのため、編集状態は別のHookかコンポーネントへ置けます。なお、useEffectの基本はReact公式のuseEffectリファレンスも参照するとよいです。

状態管理とCustom Hookの境界を決める

ただし、Custom Hookは状態管理ライブラリの代替ではありません。そこで、画面ローカルの状態を扱うのか、アプリ全体の状態を扱うのかを分けます。

  • 画面内で閉じる状態はCustom Hookでよい
  • 複数画面で共有する状態はContextや状態管理ライブラリを検討する
  • サーバーデータはキャッシュ戦略も含めて設計する
  • Hook名は `useUserQuery` や `useFormDraft` のように責務を表す

さらに、レビューではHookの戻り値が増えすぎていないかを見ます。戻り値が多い場合、そのHookは複数の責務を抱えている可能性があります。

テストしやすいCustom Hook設計にする

また、Custom Hookを作るなら、テストしやすさも確認します。入力、状態遷移、外部APIへの依存が見えると、テストの粒度を決めやすくなります。

  • Hook内部で直接windowやlocalStorageへ依存しすぎない
  • API関数を引数やモジュール境界で差し替えられるようにする
  • 非同期処理はloading、error、dataの状態を明確にする
  • UI文言や表示レイアウトはHookへ入れない

例えば、ReactのテストではReact Testing Libraryを使う場面が多いです。一方で、Hook単体で見るか、利用コンポーネント越しに見るかは、責務の大きさで判断します。

レビューで確認したいチェックリスト

  • Hook名が責務を説明しているか
  • UIの都合と業務ロジックが混ざっていないか
  • useEffectの依存配列が自然に読めるか
  • 戻り値が多すぎないか
  • 同じHookを使う画面同士が不要に結合していないか
  • テストで状態遷移を確認できるか

まとめ:Custom Hookは責務を名前で固定する

最後に、React Custom Hook 設計では、切り出すこと自体を目的にしないことが重要です。UIから隠したい状態と副作用を決め、Hook名で責務を表します。

その結果、よいCustom Hookはコンポーネントを薄くします。同時に、レビューとテストの対象を明確にします。

React/TypeScript案件では、Hook設計、状態管理、useEffectの扱いが保守性に直結します。今の経験を活かせる現場や、レビュー文化のある案件を相談したい方は、技術テーマを含めて一度話してみてください。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア