カスタムHook+ロジックHook+UIの3層構造が破綻していないか?

Reactの設計で責務分離を進めていくと、しばしば以下のような3層構造が形成されます。

  • UIコンポーネント:表示責務を担う最上層
  • ロジックHook(カスタムHook):状態・副作用・UI制御をまとめる中間層
  • ドメインHook(ロジックの再利用層):データ取得・変換・永続化処理などを扱う基盤層

このような構造は一見、再利用性・テスト容易性・責務明確性を向上させる理想的なアーキテクチャに思えます。しかし、現実のプロジェクトでは以下のような破綻が多く見られます。

  • Hookの粒度が大きくなりすぎる
  • 責務の分離より責務の隠蔽が進む
  • Hook間の依存方向が不明瞭・密結合になる

この記事では、レビューアーが3層Hook構造を読む際に注視すべき構造的な破綻の兆候と、分離が正しく機能しているかどうかの判断基準を整理します。

3層構造の典型構成とその利点

まずは意図された3層構造の正常な例を整理します。

useUserStore.ts(ドメインHook)
export const useUserStore = () => {
  const [user, setUser] = useState<User | null>(null);
  const loadUser = async () => {
    const res = await fetch('/api/user');
    const data = await res.json();
    setUser(data);
  };
  return { user, loadUser };
};
useProfileLogic.ts(ロジックHook)
export const useProfileLogic = () => {
  const { user, loadUser } = useUserStore();
  const isAdmin = user?.role === 'admin';
  return { user, isAdmin, loadUser };
};
ProfileCard.tsx(UIコンポーネント)
export const ProfileCard = () => {
  const { user, isAdmin, loadUser } = useProfileLogic();

  useEffect(() => {
    loadUser();
  }, [loadUser]);

  return (
    <div>
      <h2>{user?.name}</h2>
      {isAdmin && <p>管理者</p>}
    </div>
  );
};

このように構成された場合、

  • UIは状態や副作用を意識せず表示に専念できる
  • 状態管理は1層に閉じ、再利用性が高い
  • ビジネスロジックが明示的にHook化され、テスト可能

となり、構造としては理想に近い状態です。

3層構造とは

UI → ロジック → ドメイン という3つの責務レイヤーに分離された設計を指し、ReactではコンポーネントロジックHook状態HookやデータHookの構成としてよく現れる。

構造破綻の例1:中間Hookの肥大化と命名の曖昧化

以下は、ロジックHookがドメインロジックとUI状態を巻き込んで肥大化している例です。

useDashboard.ts
export const useDashboard = () => {
  const [searchWord, setSearchWord] = useState('');
  const [logs, setLogs] = useState<LogEntry[]>([]);
  const { user, loadUser } = useUserStore();

  const searchLogs = async () => {
    const res = await fetch(`/api/logs?q=${searchWord}`);
    const data = await res.json();
    setLogs(data);
  };

  return {
    user,
    isAdmin: user?.role === 'admin',
    logs,
    searchWord,
    setSearchWord,
    searchLogs,
    loadUser,
  };
};

@Reviewer: useDashboardは、データ取得・フィルタ条件・認可判定・状態保持がすべて集約されており、責務が曖昧です。状態ごとに責務を切り出し、構造の肥大化を防ぐ必要があります。

構造破綻の例2:Hook間の依存関係が逆転・循環

useApp.ts
export const useApp = () => {
  const { sidebarOpen } = useSidebarState();  // UIの状態
  const { user } = useUserStore();           // ドメイン層

  // ドメイン層がUI層に依存してしまっている
  return { user, sidebarOpen };
};
@Reviewer:
useApp内でUI表示の状態(sidebarOpen)とドメインロジック(user)が混在しており、Hook間の責務と依存方向が崩れています。UI状態はUIに近い階層に保持し、構造の階層を守ってください。

これは3層構造が逆転してしまった例です。本来、UI層が状態Hookに依存するべきであり、ドメイン層からUI状態にアクセスするのは構造違反です。

構造正常系と破綻系の違い

UML Diagram

UML Diagram

Hook構造のレビュー観点チェックリスト

3層構造が健全に保たれているかを判断するため、以下の観点を確認してください。

  • ロジックHookがUIとドメインを接続する責務に収まっているか?
  • ドメインHookが状態管理や副作用制御だけを担っているか?
  • Hook間の依存関係が下方向に限定されているか?(UI → ロジック → ドメイン)
  • Hook同士の責務が重複していないか?
  • 各Hookの命名が責務と一致しているか?

3層構造を導入しただけで設計が良くなるわけではありません。むしろ、Hookを分離すればするほど、構造の一貫性と責任の明示性がより重要になります。

レビューアーは、コード量の多寡ではなく、層の意図が正しく実装に表れているかという視点でHook構造を読み解くことが求められます。