36
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】コンポーネント設計パターン6選 - 小〜中規模開発

Posted at

はじめに

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を作ろうとすると時間がかかりすぎます。よく使う部品から段階的に整備していきましょう。

優先順位をつける

  1. ボタン(Primary、Secondary、Danger など)
  2. 入力フィールド・セレクトボックス
  3. カード・パネル
  4. モーダル・ダイアログ
  5. その他(タブ、アコーディオンなど)

既存ライブラリのカスタマイズも検討
ゼロから作るのではなく、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() … 投稿一覧の取得・フィルタ・ソートなどのロジック
  • PostListusePosts()から渡されたデータを表示するだけ

メリット

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コンポーネントがHeaderBodyFooterというサブコンポーネントを持ち、柔軟な構造を実現しています。

まとめ

この記事では、Reactのコンポーネント設計で使われる6つの代表的なパターンを紹介しました。

  • Atomic Design: パーツの粒度で階層化
  • Presentational / Container: 見た目とロジックを分離
  • Feature / Domain ベース: 機能単位でまとめる
  • Design System / UI Kit: 一貫したUIを作る
  • Custom Hooks: ロジックを再利用可能にする
  • Compound Components: 柔軟な構造を作る

大切なのは、プロジェクトの規模や要件に合わせて、適切なパターンを選択することです。最初から完璧を目指さず、段階的に導入していくことで、メンテナンスしやすい設計が実現できますね。

ぜひ、自分のプロジェクトに合った設計パターンを見つけて、実践してみてください。

参考リンク

36
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?