はじめに
React のコンポーネント設計において、長らく定石とされてきた Container / Presentational (C/P) パターン。
しかし、Hooks の登場、そして RTK Query や SWR のような高機能なデータフェッチライブラリの普及により、その前提は変化しています。
本記事では、「今、C/P パターンをどう解釈し、どう適用すべきか?」 という視点にフォーカスして考察します。
※ 具体的な TypeScript の型定義や実装テクニックについては、後編記事「【API 型定義 × UI 設計の最適解 #2】RTK Query × TypeScript 実践:型安全とパフォーマンスを両立する実装テクニック」で解説しています。
※ 本記事のコード例は RTK Query を使用していますが、コロケーションの考え方自体は SWR や TanStack Query 等、キャッシュ機構を持つデータフェッチライブラリ全般に適用できます。
議論の背景:前提環境の変化
かつての React 環境と、現在の環境(Hooks + RTK Query + TS)では、コンポーネントを取り巻く状況が異なります。
| かつて (Redux, Class Component) | 現在 (Hooks, RTK Query, TS) | |
|---|---|---|
| ロジックの分離 | Class (Container) vs Functional (Presentational) で分けるのが必須だった | Custom Hooks で自由自在に切り出せる |
| データ取得 | Redux Middleware (Thunk/Saga) で一元管理し、Props で流し込む | 各コンポーネント内で Hooks (RTK Query) を呼ぶ (Colocation) |
| 型安全性 | PropTypes や手動のインターフェース定義 | openapi-typescript 等による自動生成型との連携 |
この変化により、「とりあえず C/P に分ける」という思考停止的な適用は、かえってコードを複雑にする要因となってきました。
C/P パターンの問題点
C/P パターンには、以下のような問題があります。
Props バケツリレー
親 (Container) でデータを取得し、子・孫へ渡していくと、間に挟まるコンポーネントは使いもしない Props を右から左へ受け渡すためだけに修正が必要になります。
Props 境界での型推論の断絶
Props で data を渡す場合、型推論は引き継がれないため、Props の型を明示的に定義する必要があります。
// Container
const { data: user } = useGetUserQuery(userId);
// ↑ ここでは user: User 型が推論されている
// しかし Presentational に渡すと...
<UserCardView name={user.name} avatarUrl={user.avatarUrl} />;
// ↑ Props の型は手動で定義が必要(API 型をインポートすれば追従可能だが、ひと手間かかる)
API 側で name のフィールド名や型が変わっても、View の Props 定義には自動的にエラーが出ません。
API 型をインポートして Props に使えば追従は可能ですが、手動での型定義が必要になります。
解決策:Co-location を基本とする
これらの問題を解決するアプローチとして、Co-location(コロケーション)を基本 とするスタイルが有効です。
Co-location とは
「データを使う場所で、データを取得する」 という考え方です。
RTK Query 等のキャッシュ機能があるライブラリは、これを強力に後押しします。
Co-location のメリット:
- コンポーネントが自己完結する(どこに配置しても動く)
- 不要な Props バケツリレーが消滅する
- useQuery の戻り値から型推論がそのまま効く(型が死なない)
- Hooks により、ロジックと View の分離は 1 ファイル内でも十分に可能
周辺技術との整合性
設計判断においては、テストや周辺ツールとの相性も重要です。
Storybook との兼ね合い
「View だけ切り出さないと Storybook が書けないのでは?」という懸念は、現在は解消されています。
msw-storybook-addon 等を使えば、API 通信を行うコンポーネントもそのまま Storybook でカタログ化可能です。
- 昔: データを Props で受け取る Pure Component を作らないとテストしづらかった。
- 今: MSW で API をモックすれば、データ取得ロジック入りのコンポーネントごとその場でテストできる。
これにより、「テストのためだけの過剰なコンポーネント分割」は不要になりました。
SSR / Server Components (RSC) との距離感
Next.js App Router (RSC) の登場で「データ取得はサーバー側」という揺り戻しも起きています。
しかし、リッチなインタラクションを伴う業務アプリ等では、依然としてクライアントサイドでのデータ管理 (RTK Query) が主役になる場面も多いです。
- 静的なページ / 初期表示: RSC や SSR でデータを用意
- インタラクティブな機能: RTK Query + Hooks で動的に管理
このように使い分ける場合でも、「ロジックを Hooks に寄せる」 という基本姿勢は共通して有効です。
チーム開発のためのガイドライン
これらを踏まえた、現場でのコンポーネント設計指針の例です。
-
まずは 1 コンポーネントで完結させる (Co-location)
- API Hooks もコンポーネント内で直接呼ぶ。
- ロジックが複雑なら Custom Hooks に逃がす。
-
Props で API データを原則渡さない
- 特定の API に依存するコンポーネントでは、データを渡すのではなく、ID を渡して子コンポーネント側で取得させる。
- 複数コンポーネントから同じ ID で
useQueryを呼んでも、RTK Query がキャッシュするので問題ない。 - ただし、以下は例外:
-
汎用コンポーネント(
<Card>,<Avatar>など):複数の API で使い回すため、特定の Hooks に依存できない。 -
一覧表示:各行が個別に API を呼ぶとリクエスト数が増大するため、親で一括取得して
mapで子に渡す方が効率的。
-
汎用コンポーネント(
-
ファイルが長くなったらロジックを Hooks に切り出す
- View の分離より先に、Custom Hooks への分離を検討する。
おわりに
Hooks の登場により、C/P パターンは「必須の構造」から「選択肢のひとつ」へと役割が変わりました。
コロケーションを採用すると、Props の型定義が減り、親コンポーネントからデータを受け取る必要がなくなるというメリットがあります。一方で、C/P パターンにも View の再利用性やテスト容易性といった利点があるため、プロジェクトの状況に応じて使い分けるのが現実的です。
次のアクション
この設計思想を、TypeScript × RTK Query 環境で具体的にどうコードに落とし込むか?
特に**「Props の型定義をどうサボるか」「無駄な再レンダリングをどう防ぐか」**といった実装詳細については、以下の記事で解説します。
👉 【API 型定義 × UI 設計の最適解 #2】RTK Query × TypeScript 実践:型安全とパフォーマンスを両立する実装テクニック
参考にした記事・資料
-
Dan Abramov, "Presentational and Container Components"
元祖 C/P パターンの記事。2019 年の追記で「Hooks によってこの分割は必須ではなくなった/パターンをドグマとして押し付けるべきではない」とコメントしている。
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 -
React Patterns (patterns.dev), "Container/Presentational Pattern"
C/P パターンの定義とメリット/デメリットの整理に加えて、「多くのケースではカスタムフック+ Hooks で代替できる」と明言されている。
https://www.patterns.dev/react/presentational-container-pattern/ -
Shinya Fujino (訳), 「コンテナ・プレゼンテーションパターン」『フロントエンドのデザインパターン』
パターンの基本的な説明と、小規模アプリでは複雑になりがちという注意点が日本語でまとまっている。
https://zenn.dev/morinokami/books/learning-patterns-1/viewer/presentational-container-pattern -
Storybook docs, "Mock Service Worker | Storybook integrations"
msw-storybook-addonを使って、API モック込みでコンポーネントをカタログ化・テストする方法。これがあれば Pure Component にこだわらずともツール上で表示確認が可能。
https://storybook.js.org/addons/msw-storybook-addon -
Next.js docs, "Fetching Data on the Server"
Next.js App Router においても、データは「必要なコンポーネントで直接取得する(コロケーション)」ことが推奨されているという公式記述。
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server