はじめに
Reactでアプリケーションを開発していると、コンポーネントの整理方法に悩むことはありませんか。「どこに何を置くべきか」「どう分割すればいいのか」といった疑問は、プロジェクトが大きくなるほど重要になってきます。
この記事では、Reactのコンポーネント設計でよく使われる6つの代表的なパターンを紹介します。それぞれのパターンの概要、メリット、そして小〜中規模開発での実践的な使い方を解説していきますね。
この記事で学べること
- 各設計パターンの基本的な考え方
- 小〜中規模プロジェクトでの現実的な使い方
- パターンを組み合わせた実践的な構成例
特に、小〜中規模の開発では「Atomic Design + Feature ベース構成 + Custom Hooks + Presentational/Container」の組み合わせが現実的で効果的です。この記事を読めば、自分のプロジェクトに最適な設計パターンを選べるようになります。
1. Atomic Design - パーツの粒度で整理する
概要と5つの階層
Atomic Designは、UIコンポーネントを「パーツの粒度」で階層化する設計手法です。化学の原子・分子の考え方を借りて、小さな部品から大きな構造を組み立てていきます。
5つの階層は次のように定義されます。
Atoms(原子)
最小単位のUI部品です。ボタン、ラベル、入力フィールドなど、それ以上分割できない基本的な要素を指します。
Molecules(分子)
Atomsを組み合わせた小さな単位です。例えば、ラベルと入力フィールドを組み合わせたフォームグループなどが該当します。
Organisms(有機体)
MoleculesやAtomsを組み合わせた、ある程度大きな意味のある塊です。ヘッダー、サイドバー、カードリストなどが該当します。
Templates(テンプレート)
Organismsを配置して作られる、ページの骨組みです。実際のデータは入っていない状態のレイアウトを定義します。
Pages(ページ)
Templatesに実際のデータを流し込んだ完成形です。具体的なページとして機能します。
メリット
Atomic Designを採用することで、次のようなメリットが得られます。
再利用しやすいコンポーネントが自然に増える
小さな粒度から設計するため、汎用的なコンポーネントが自然と蓄積されていきます。新しい画面を作る際も、既存の部品を組み合わせるだけで済むケースが増えますね。
コンポーネントの責務の大きさが揃いやすい
階層が明確なので、各コンポーネントがどの程度の責務を持つべきかが判断しやすくなります。「このコンポーネントは大きすぎるかも」といった感覚的な判断ではなく、階層に基づいた客観的な判断ができます。
デザイナーとの共通言語にしやすい
デザインシステムとの相性が良く、デザイナーとエンジニアの間で共通の用語を使ってコミュニケーションできます。
小〜中規模での実践的な使い方
フルで厳密にやり過ぎると運用が重くなるため、次のような緩い運用がおすすめです。
緩い分類で十分
src/
components/
atoms/ # ボタン、入力フィールドなど
molecules/ # フォームグループ、検索バーなど
organisms/ # ヘッダー、カードリストなど
pages/ # 実際のページコンポーネント
TemplatesとPagesを明確に分けずに、pages/ディレクトリだけで管理するのが現実的です。
段階的な導入
最初から全てを完璧に分類しようとせず、まずは「Atoms と Molecules だけきれいにする」と決めるのが運用しやすいですね。共通部品として使われるものから順に整理していきましょう。
2. Presentational / Container パターン - 見た目とロジックを分離する
概要と役割分担
Presentational / Container パターン(Smart / Dumb コンポーネントとも呼ばれます)は、コンポーネントを「見た目」と「ロジック」で分ける古典的な設計手法です。
Presentational コンポーネント(見た目担当)
UIの表示だけに専念するコンポーネントです。propsでデータやイベントハンドラーを受け取り、それを表示します。状態管理やデータ取得は行いません。
Container コンポーネント(ロジック担当)
データ取得、状態管理、ビジネスロジックを担当するコンポーネントです。Presentationalコンポーネントにデータを渡し、表示を制御します。
メリット
この分離によって、次のようなメリットが得られます。
UIのテスト・差し替えがしやすい
見た目とロジックが分離しているため、UIのテストはpropsを渡すだけで完結します。デザイン変更時もPresentationalコンポーネントだけを修正すればよいので、影響範囲が明確です。
役割分担が明確になる
デザイナーやフロントエンド寄りの人はPresentationalコンポーネントだけを触ればよく、バックエンド寄りの人はContainerコンポーネントに集中できます。
ロジックが散らばりにくい
データ取得や状態管理のロジックがContainerに集約されるため、コードベース全体の見通しが良くなります。
小〜中規模での実践的な使い方
小〜中規模のプロジェクトでは、次のようなルールで運用すると効果的です。
画面単位でContainerを決める
features/
posts/
PostsPage.tsx # Container(データ取得・状態管理)
components/
PostList.tsx # Presentational(表示のみ)
PostItem.tsx # Presentational(表示のみ)
PostFilter.tsx # Presentational(表示のみ)
データ取得はPage/Containerに閉じ込める
APIの呼び出しや状態管理は、ページレベルのコンポーネントに集中させます。下位のコンポーネントは、propsで受け取ったデータを表示するだけにしましょう。
コード例
// Container コンポーネント(PostsPage.tsx)
import { useState, useEffect } from 'react';
import { PostList } from './components/PostList';
export const PostsPage = () => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchPosts().then(data => {
setPosts(data);
setLoading(false);
});
}, []);
const handleDelete = (id) => {
// 削除処理のロジック
setPosts(posts.filter(post => post.id !== id));
};
return (
<div>
<h1>投稿一覧</h1>
<PostList
posts={posts}
loading={loading}
onDelete={handleDelete}
/>
</div>
);
};
// Presentational コンポーネント(PostList.tsx)
export const PostList = ({ posts, loading, onDelete }) => {
if (loading) {
return <div>読み込み中...</div>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<button onClick={() => onDelete(post.id)}>削除</button>
</li>
))}
</ul>
);
};
この例では、PostsPageがデータ取得と状態管理を担当し、PostListは受け取ったpropsを表示するだけになっています。
3. Feature / Domain ベース構成 - 機能単位でまとめる
概要とフォルダ構成例
Feature / Domain ベース構成は、コンポーネントやロジックを「機能ごと」や「ドメインごと」にまとめる考え方です。画面や機能に関連するファイルを1つのフォルダにまとめることで、関連性の高いコードを近くに配置します。
src/
features/
posts/ # 投稿機能
components/ # 投稿関連のコンポーネント
PostList.tsx
PostItem.tsx
hooks/ # 投稿関連のカスタムフック
usePosts.ts
api/ # 投稿関連のAPI呼び出し
posts.api.ts
types/ # 投稿関連の型定義
post.types.ts
auth/ # 認証機能
components/
LoginForm.tsx
hooks/
useAuth.ts
api/
auth.api.ts
shared/ # 共通で使う部品
components/
atoms/
molecules/
hooks/
utils/
図解: フォルダ構成の例
メリット
Feature ベース構成には、次のようなメリットがあります。
迷子になりにくい
小〜中規模でも「画面や機能ごとに責務がまとまる」ので、どのファイルがどこにあるか迷いにくくなります。新しいメンバーがプロジェクトに参加しても、機能名から直感的にファイルを探せますね。
将来の拡張性が高い
機能単位で切り出したり、Micro Frontendのように分離したりしやすい構造です。将来的にプロジェクトが大きくなっても対応しやすくなります。
1画面に必要なものがまとまる
1つの画面を実装・修正する際に必要なファイルが、ほぼ1つのフォルダに揃っています。複数のフォルダを行き来する必要が減り、開発効率が向上します。
小〜中規模での実践的な使い方
まずはfeaturesフォルダを作る
いきなり完璧な構成を目指すのではなく、まずはfeatures/フォルダを作って、画面や機能ごとにフォルダを切るだけでも大きな効果があります。
共通UIはsharedに寄せる
複数の機能で使う共通のUIコンポーネントはshared/componentsに配置します。ここにAtomic Designの階層を導入すると、さらに整理しやすくなりますね。
shared/
components/
atoms/
Button.tsx
Input.tsx
molecules/
FormGroup.tsx
SearchBar.tsx
機能の粒度は柔軟に決める
「この機能は大きすぎるかも」と感じたら、さらにサブフォルダで分割するなど、プロジェクトの規模に応じて柔軟に調整しましょう。
4. Design System / UI Kit ベース - 一貫したUIを作る
概要
Design System / UI Kit ベースは、アプリケーション全体で一貫したUIを実現するための「コンポーネントライブラリ中心」の考え方です。ボタン、フォーム、モーダルなどの基本的なUI部品を「自前のUIライブラリ」として整備します。
外部ライブラリを使う場合も含め、次のような選択肢があります。
- Tailwind CSS + 自作コンポーネント
- Material-UI(MUI)
- Chakra UI
- Ant Design
メリット
Design Systemを導入することで、次のようなメリットが得られます。
デザインの一貫性が出る
色、フォント、余白、アニメーションなどが統一され、アプリケーション全体で一貫したユーザー体験を提供できます。
画面追加のコストが下がる
新しい画面を作る際も、既存の部品を組み合わせるだけで済むため、開発スピードが向上します。デザインに悩む時間も減りますね。
テーマ切り替えがしやすい
ライトモード・ダークモードの切り替えや、ブランドカラーの変更などが、中央で管理された設定を変えるだけで実現できます。
小〜中規模での実践的な使い方
最初から全部作り込まない
小〜中規模のプロジェクトでは、最初から完璧なDesign Systemを作ろうとすると時間がかかりすぎます。よく使う部品から段階的に整備していきましょう。
優先順位をつける
- ボタン(Primary、Secondary、Danger など)
- 入力フィールド・セレクトボックス
- カード・パネル
- モーダル・ダイアログ
- その他(タブ、アコーディオンなど)
既存ライブラリのカスタマイズも検討
ゼロから作るのではなく、MUIやChakra UIをベースにして、必要な部分だけカスタマイズする方が効率的な場合もあります。
// 共通ボタンコンポーネントの例
// shared/components/atoms/Button.tsx
type ButtonVariant = 'primary' | 'secondary' | 'danger';
interface ButtonProps {
variant?: ButtonVariant;
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export const Button = ({
variant = 'primary',
children,
onClick,
disabled = false
}: ButtonProps) => {
const baseClass = 'px-4 py-2 rounded font-medium transition-colors';
const variantClass = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600'
};
return (
<button
className={`${baseClass} ${variantClass[variant]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
5. Custom Hooks パターン - ロジックを再利用可能にする
概要
Custom Hooks パターンは、React HooksのuseXxxに状態管理やビジネスロジックを閉じ込め、コンポーネントは表示に集中させる設計手法です。
ロジックとUIを分離することで、次のような役割分担が実現します。
-
usePosts()… 投稿一覧の取得・フィルタ・ソートなどのロジック -
PostList…usePosts()から渡されたデータを表示するだけ
メリット
Custom Hooksを活用することで、次のようなメリットが得られます。
ロジックの再利用性が高い
同じデータ取得ロジックを複数のコンポーネントで使いたい場合、Custom Hookにまとめておけば簡単に再利用できます。
テストがしやすい
Hook単体でテストできるため、UIとロジックを分けてテストできます。複雑なロジックほど、この恩恵が大きいですね。
コンポーネントの見通しが良くなる
コンポーネント内にロジックを書かず、Hookから受け取ったデータを表示するだけにすることで、JSXの記述が中心となり読みやすくなります。
小〜中規模での実践的な使い方
API呼び出しが出たらHookを切る
「API呼び出しが出てきたら、そのたびにuseXxxを切る」くらいのシンプルなルールで運用すると、自然とロジックがまとまっていきます。
Featureフォルダ内にhooksディレクトリを作る
features/
posts/
hooks/
usePosts.ts # 投稿一覧の取得
usePostDetail.ts # 投稿詳細の取得
usePostForm.ts # 投稿フォームのロジック
コード例
// features/posts/hooks/usePosts.ts
import { useState, useEffect } from 'react';
import { fetchPosts } from '../api/posts.api';
export const usePosts = () => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const loadPosts = async () => {
try {
setLoading(true);
const data = await fetchPosts();
setPosts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadPosts();
}, []);
const deletePost = (id) => {
setPosts(posts.filter(post => post.id !== id));
};
return { posts, loading, error, deletePost };
};
// features/posts/PostsPage.tsx
import { usePosts } from './hooks/usePosts';
import { PostList } from './components/PostList';
export const PostsPage = () => {
const { posts, loading, error, deletePost } = usePosts();
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return (
<div>
<h1>投稿一覧</h1>
<PostList posts={posts} onDelete={deletePost} />
</div>
);
};
この例では、usePostsHookがデータ取得と状態管理のロジックを担当し、PostsPageコンポーネントは表示に集中しています。
6. Compound Components パターン - 柔軟な構造を作る
概要
Compound Components パターンは、親コンポーネントの中に関連するサブコンポーネントをネストして使うパターンです。親子関係を持つコンポーネント群が協調して動作し、柔軟なレイアウトを実現します。
<Modal>
<Modal.Header>タイトル</Modal.Header>
<Modal.Body>内容</Modal.Body>
<Modal.Footer>
<Button>キャンセル</Button>
<Button variant="primary">保存</Button>
</Modal.Footer>
</Modal>
メリット
Compound Componentsには、次のようなメリットがあります。
APIが直感的で読みやすい
HTMLの構造に近い形で記述できるため、コードの意図が伝わりやすくなります。どの部分がヘッダーで、どこがフッターなのかが一目瞭然ですね。
柔軟なレイアウト変更に強い
サブコンポーネントの順序を変えたり、一部を省略したりすることが簡単にできます。固定的なpropsの構造に縛られません。
Design Systemでよく使われる
モーダル、カード、タブ、アコーディオンなど、構造が複雑なUIコンポーネントの実装に適しています。
小〜中規模での実践的な使い方
構造が決まっているUIから導入
すべてのコンポーネントをCompound Componentsにする必要はありません。次のような、構造が決まっているUIから少しずつ導入していきましょう。
- モーダル・ダイアログ
- カード(ヘッダー、本文、フッター)
- タブ
- アコーディオン
コード例
// shared/components/molecules/Card.tsx
import { createContext, useContext } from 'react';
const CardContext = createContext({});
export const Card = ({ children, className = '' }) => {
return (
<CardContext.Provider value={{}}>
<div className={`border rounded-lg shadow ${className}`}>
{children}
</div>
</CardContext.Provider>
);
};
Card.Header = ({ children, className = '' }) => {
return (
<div className={`px-6 py-4 border-b ${className}`}>
{children}
</div>
);
};
Card.Body = ({ children, className = '' }) => {
return (
<div className={`px-6 py-4 ${className}`}>
{children}
</div>
);
};
Card.Footer = ({ children, className = '' }) => {
return (
<div className={`px-6 py-4 border-t bg-gray-50 ${className}`}>
{children}
</div>
);
};
// 使用例
<Card>
<Card.Header>
<h2>記事タイトル</h2>
</Card.Header>
<Card.Body>
<p>記事の内容がここに入ります。</p>
</Card.Body>
<Card.Footer>
<Button>詳細を見る</Button>
</Card.Footer>
</Card>
この例では、CardコンポーネントがHeader、Body、Footerというサブコンポーネントを持ち、柔軟な構造を実現しています。
まとめ
この記事では、Reactのコンポーネント設計で使われる6つの代表的なパターンを紹介しました。
- Atomic Design: パーツの粒度で階層化
- Presentational / Container: 見た目とロジックを分離
- Feature / Domain ベース: 機能単位でまとめる
- Design System / UI Kit: 一貫したUIを作る
- Custom Hooks: ロジックを再利用可能にする
- Compound Components: 柔軟な構造を作る
大切なのは、プロジェクトの規模や要件に合わせて、適切なパターンを選択することです。最初から完璧を目指さず、段階的に導入していくことで、メンテナンスしやすい設計が実現できますね。
ぜひ、自分のプロジェクトに合った設計パターンを見つけて、実践してみてください。