Hooks時代でもPresenter分離が有効な理由|Storybook・テスト・分業の観点から

開発

「Hooksがあるから、Presenter/Containerパターンはもう古い」

そう思っていませんか?Dan Abramov自身が「もう推奨しない」と言っているし、カスタムhooksに抽出すれば同じ分離ができる。理屈としては正しいです。

でも、Storybookを使っていると話が変わります。テスト戦略を考えると話が変わります。デザイナーとの分業があると話が変わります。

この記事では、Hooks時代でもPresenter分離が有効なケースと、その判断基準を解説します。Props Soup vs Compound Componentの続編として、コンポーネント設計の別の軸を掘り下げます。

Dan Abramovの見解とその限界

まず、Dan Abramovが元記事に追記した内容を確認しましょう。

「このパターンはもう推奨しない。コードベースで自然なら使っても良いが、必要もないのに教義のように強制されるのを見すぎた」

理由は明確で、Hooksで同じ分離ができるからです。

// ❌ 昔: Container + Presentational
function OrderSummaryContainer() {
  const { data } = useOrder()
  return <OrderSummaryPresenter order={data} />
}

// ✅ 今: Custom Hook + 1つのコンポーネント
function OrderSummary() {
  const { data } = useOrder()  // ロジックはhookに抽出
  return <div>...</div>        // 描画はここ
}

これ、理屈としては完全に正しいんですよね。でも「現場で使ってみると…」という話が抜けてる気がするんです。

筆者

見落とされている3つの観点

Dan Abramovの見解が見落としているのは、以下の3点です。

  1. Storybookとhooksの相性問題
  2. 分業ワークフロー(デザイン先行開発)
  3. テスト分類の明確さ

順番に見ていきましょう。

Storybookとhooksの相性問題

Storybook公式の思想を確認すると、こう書かれています。

「Storybookはデータ、API、ビジネスロジックなしでコンポーネントを実装できる」

これ、まさに純粋なPresenterの定義そのものです。

問題: hooks入りコンポーネントはStoryが書きにくい

GitHub Issue #11227やMediumの記事で議論されている通り、hooks入りコンポーネントには課題があります。

「useQueryやuseMutationなどのhooksを使うコンポーネントは、Storybookで動かせないことが多い」

「RESTエンドポイントを呼ぶhookがあると、テストやStorybook環境でレンダリングに失敗する」

実際に書くとこうなります。

// hooks入りコンポーネント
function UserProfile() {
  const { data, isLoading } = useUser()  // APIを呼ぶ
  const { mutate } = useUpdateUser()     // 更新もする

  if (isLoading) return <Skeleton />
  return <div>{data.name}</div>
}

// ↓ Storyを書くには...
export const Default: Story = {
  decorators: [
    (Story) => (
      <QueryClientProvider client={mockQueryClient}>
        <Story />
      </QueryClientProvider>
    ),
  ],
  parameters: {
    msw: {
      handlers: [
        rest.get('/api/user', (req, res, ctx) => {
          return res(ctx.json({ name: 'Test User' }))
        }),
      ],
    },
  },
}

これ、1つのStoryを書くだけでこの量。10個のコンポーネントがあったら…考えたくないですね。

筆者

解決策: Presenterを分離する

// Presenter(純粋なUI)
function UserProfilePresenter({ user, onUpdate }: Props) {
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={onUpdate}>更新</button>
    </div>
  )
}

// Container(hooks呼び出し)
function UserProfile() {
  const { data } = useUser()
  const { mutate } = useUpdateUser()
  return <UserProfilePresenter user={data} onUpdate={mutate} />
}

Storyはシンプルになります。

// Storyはmockなしで書ける
export const Default: Story = {
  args: {
    user: { name: 'Test User' },
    onUpdate: fn(),
  },
}

export const LongName: Story = {
  args: {
    user: { name: 'Very Long Name That Might Break Layout' },
    onUpdate: fn(),
  },
}

Woltの実例

Wolt Engineering Blogでは、認証フローのリフレッシュ時にこのパターンを採用しています。

「認証hooksを注入可能(injectable)にした結果、Storybookのストーリー作成がstraightforwardになった。Jestのテストもtype-unsafeなモジュールmockingが不要になった」

分業ワークフロー(デザイン先行開発)

FreeCodeCampやTSH Blogの記事では、分業のメリットが明確に述べられています。

「2人が同時に1つのものに取り組める。ジュニアがスタイルとJSXを書き、経験豊富な開発者がロジックを書ける」

「Presentationalコンポーネントはアプリのロジックを変更しないため、コードベースの知識がない人(デザイナーなど)でも見た目を変更できる」

デザイン→実装の流れ

実際のプロジェクトでは、こんな流れが多いのではないでしょうか。

  1. デザイナー: Figmaでデザイン完成
  2. フロントエンド(UI担当): デザイン通りにコンポーネント実装
  3. フロントエンド(ロジック担当): API連携、状態管理を実装
  4. 結合: ContainerでPresenterとhooksを繋ぐ

この流れで、ステップ2と3が並行して進められるのがPresenter分離の強みです。

hooksの手戻りが起きたとき

APIの仕様変更、状態管理の設計変更…手戻りは必ず起きます。

hooks入りコンポーネントの場合:

  • hooksを修正 → コンポーネント再レンダリングに影響
  • Storyが壊れる → 修正が必要
  • snapshotテストが壊れる → 更新が必要

Presenterを分離している場合:

  • hooksを修正 → Containerだけ修正
  • Presenterはpropsが同じなら何も変わらない
  • Storyもsnapshotもそのまま動く

この「影響範囲の局所化」が、大規模プロジェクトでは本当に効いてくるんですよね。

筆者

テスト戦略の明確化

Testing LibraryのrenderHookを使えば、hooksを単独でテストできます。

// hooks/useOrder.test.ts
import { renderHook, waitFor } from '@testing-library/react'
import { useOrder } from './useOrder'

test('注文データを取得できる', async () => {
  const { result } = renderHook(() => useOrder('order-123'))

  await waitFor(() => {
    expect(result.current.data).toBeDefined()
  })
})

テストの分類が明確になる

テスト対象テスト種別ツール
PresenterUIテスト、snapshotStorybook, Jest
Custom HookロジックテストrenderHook
Container統合テストReact Testing Library

DEV Communityの記事では、こう述べられています。

「Pure Componentのテストはシンプル。エラーがあればpropsを確認するだけ。このコンポーネント内でstateが更新されているか心配する必要がない」

hooks入りコンポーネントのテストは複雑

// hooks入りコンポーネントのテスト
test('ユーザー情報を表示する', async () => {
  // 1. API mockを設定
  server.use(
    rest.get('/api/user', (req, res, ctx) => {
      return res(ctx.json({ name: 'Test User' }))
    })
  )

  // 2. Providerでラップ
  render(
    <QueryClientProvider client={queryClient}>
      <UserProfile />
    </QueryClientProvider>
  )

  // 3. 非同期処理を待つ
  await waitFor(() => {
    expect(screen.getByText('Test User')).toBeInTheDocument()
  })
})

Presenterなら、こうなります。

// Presenterのテスト
test('ユーザー名を表示する', () => {
  render(<UserProfilePresenter user={{ name: 'Test User' }} />)
  expect(screen.getByText('Test User')).toBeInTheDocument()
})

「テストのために分離する」は正当なアプローチ

「テストのために設計を変えるのは本末転倒では?」と思うかもしれません。でも、Kent Beckはこう言っています。

「TDDはデザインに進化的な圧力をかける」 「テストを先に考えることで、インターフェースを先に考えることを強制する。これが良い設計の鍵だ」

Microsoft Learnの記事や学術研究でも、テスタビリティと良い設計の相関が示されています。

「テスタビリティは、カプセル化、結合、凝集性と高い相関がある」 「弱い凝集性、強い結合、冗長性、カプセル化の欠如 = テストしにくい」

つまり、テストしにくいコードは設計に問題があるシグナル。テスタビリティを設計の指針にすることは、結果的に良い設計につながります。学術研究では、Design for Testability(テスタビリティのための設計)が開発予算の約10%を節約するという結果も出ています。

判断基準: いつPresenterを分離すべきか

「中規模と大規模の区別が難しい」という問題があります。プロジェクトの規模感で判断するより、具体的な条件で判断しましょう。

Presenter分離が有効なケース

  1. Storybookを積極的に使っている

    • コンポーネントカタログとして運用
    • デザイナーがStorybookを確認する
  2. 分業がある

    • UI実装とロジック実装が別の人/タイミング
    • デザイン先行で開発している
  3. テストを重視している

    • UIの見た目テスト(snapshot)を書いている
    • hooksのロジックテストを書いている
  4. コンポーネントが再利用される

    • 同じUIで異なるデータソースを使うケース
    • デザインシステムのコンポーネント

分離しなくてよいケース

  1. Storybookを使っていない
  2. 一人で開発している
  3. コンポーネントが使い捨て(1画面でしか使わない)
  4. hooksが単純(useStateが1つだけ、など)

まとめ

「Hooksがあるからcontainer不要」という風潮は、Storybook・テスト・分業の観点が抜けていると言えます。

観点hooks入りコンポーネントPresenter分離
Storybookmockが複雑mockなしで書ける
分業並行作業が難しいUI/ロジックを分担可能
テスト統合テストになりがち単体テストが明確
手戻り影響範囲が広いPresenterは影響なし

判断基準をもう一度:

  • Storybookを使っている → 分離を検討
  • 分業がある → 分離を検討
  • テストを重視している → 分離を検討
  • 上記すべてNO → hooks入りコンポーネントで十分

Dan Abramovの「必要もないのに教義のように強制するな」という警告は正しいです。ただし、必要なケースでは依然として有効なパターンです。自分のプロジェクトの状況に合わせて判断してください。

Thanks for reading!
ReactStorybookテスト設計コンポーネント設計フロントエンド設計