はじめに

前回の記事では、ReactでUIを記述するためのJSX構文について解説しました。本記事では、Reactアプリケーションの基本単位である「コンポーネント」について詳しく解説します。

コンポーネントは、UIを独立した再利用可能なパーツに分割するための仕組みです。適切にコンポーネント化することで、コードの保守性が向上し、開発効率が大幅に改善されます。

本記事を読むことで、以下のことができるようになります。

  • 関数コンポーネントの定義と使い方
  • コンポーネントを分割するタイミングの判断
  • 再利用可能なコンポーネントの設計
  • ファイル構成のベストプラクティスの理解

実行環境・前提条件

必要な環境

  • Node.js 20.x以上
  • Viteで作成したReactプロジェクト
  • VS Code(推奨)

前提知識

  • JSXの基本構文
  • JavaScriptの関数の書き方
  • ES6のimport/export構文

コンポーネントとは何か

コンポーネントは、UIの一部を表す独立した部品です。ボタン、カード、ヘッダー、フォームなど、画面を構成する要素をそれぞれコンポーネントとして定義し、組み合わせてアプリケーション全体を構築します。

コンポーネントの特徴

Reactコンポーネントには以下の特徴があります。

  • 独立性:他のコンポーネントに影響を与えずに変更できる
  • 再利用性:同じコンポーネントを複数の場所で使用できる
  • 組み合わせ可能:コンポーネントの中に別のコンポーネントを含められる

関数コンポーネントとは

Reactコンポーネントには「関数コンポーネント」と「クラスコンポーネント」の2種類がありますが、2025年現在、関数コンポーネントが標準的な書き方です。

1
2
3
4
// 関数コンポーネントの基本形
function Greeting() {
  return <h1>Hello, React!</h1>;
}

関数コンポーネントは、JSXを返す関数として定義します。React 16.8で導入されたHooksにより、関数コンポーネントでも状態管理やライフサイクル処理が可能になりました。

関数コンポーネントの定義方法

関数コンポーネントを定義する方法は複数あります。

通常の関数宣言

1
2
3
function Button() {
  return <button>クリック</button>;
}

最もシンプルで読みやすい書き方です。関数の巻き上げ(hoisting)が適用されるため、定義より前に使用することも可能です。

アロー関数

1
2
3
const Button = () => {
  return <button>クリック</button>;
};

ES6のアロー関数を使用した書き方です。多くのプロジェクトで採用されています。

アロー関数(省略形)

1
const Button = () => <button>クリック</button>;

JSXが1行で収まる場合は、returnと波括弧を省略できます。

どの書き方を選ぶべきか

プロジェクトやチームで統一されていれば、どの書き方でも問題ありません。一般的には以下のガイドラインがあります。

  • シンプルなコンポーネントはアロー関数の省略形
  • ロジックを含むコンポーネントはアロー関数または通常の関数宣言
  • チームで書き方を統一する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// シンプルなコンポーネント
const Badge = () => <span className="badge">NEW</span>;

// ロジックを含むコンポーネント
const UserCard = ({ user }) => {
  const formattedDate = new Date(user.joinedAt).toLocaleDateString('ja-JP');
  
  return (
    <div className="user-card">
      <h2>{user.name}</h2>
      <p>登録日: {formattedDate}</p>
    </div>
  );
};

コンポーネントの使い方

定義したコンポーネントは、JSXタグとして使用します。

基本的な使い方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function Greeting() {
  return <h1>こんにちは</h1>;
}

function App() {
  return (
    <div>
      <Greeting />
      <p>Reactの世界へようこそ</p>
    </div>
  );
}

期待される結果

1
2
3
4
<div>
  <h1>こんにちは!</h1>
  <p>Reactの世界へようこそ</p>
</div>

コンポーネントの命名規則

コンポーネント名は必ず大文字で始めます。これはReactがHTML要素とカスタムコンポーネントを区別するためのルールです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 正しい:大文字で始まる
function MyButton() {
  return <button>クリック</button>;
}

// 誤り:小文字で始まるとHTML要素として解釈される
function myButton() {
  return <button>クリック</button>;
}

// 使用時
<MyButton />  // Reactコンポーネント
<button />    // HTML要素

コンポーネントの再利用

同じコンポーネントを複数回使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Star() {
  return <span></span>;
}

function Rating() {
  return (
    <div>
      <Star />
      <Star />
      <Star />
      <Star />
      <Star />
    </div>
  );
}

コンポーネントのネスト(入れ子構造)

コンポーネントの中に別のコンポーネントを含めることで、複雑なUIを構築できます。

コンポーネントツリー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Header() {
  return (
    <header>
      <Logo />
      <Navigation />
    </header>
  );
}

function Logo() {
  return <h1>My App</h1>;
}

function Navigation() {
  return (
    <nav>
      <NavItem label="ホーム" />
      <NavItem label="About" />
      <NavItem label="Contact" />
    </nav>
  );
}

function NavItem({ label }) {
  return <a href="#">{label}</a>;
}

function App() {
  return (
    <div>
      <Header />
      <main>
        <h2>メインコンテンツ</h2>
      </main>
    </div>
  );
}

このコードは以下のようなコンポーネントツリーを形成します。

App
├── Header
│   ├── Logo
│   └── Navigation
│       ├── NavItem ("ホーム")
│       ├── NavItem ("About")
│       └── NavItem ("Contact")
└── main
    └── h2

コンポーネントの定義場所

コンポーネントは必ずトップレベルで定義します。コンポーネント内部で別のコンポーネントを定義してはいけません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 誤り:コンポーネント内部で定義
function Parent() {
  // 毎回再定義され、パフォーマンスの問題が発生
  function Child() {
    return <p>子コンポーネント</p>;
  }
  
  return <Child />;
}

// 正しい:トップレベルで定義
function Child() {
  return <p>子コンポーネント</p>;
}

function Parent() {
  return <Child />;
}

コンポーネントの分割基準

UIをどの単位でコンポーネントに分割するかは、React開発における重要な判断です。

分割を検討すべきタイミング

以下の条件に当てはまる場合、コンポーネントの分割を検討しましょう。

再利用性

同じUIパターンが複数箇所で使われる場合、コンポーネントとして抽出します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 分割前:同じ構造が繰り返される
function ProductList() {
  return (
    <div>
      <div className="product-card">
        <h3>商品A</h3>
        <p>1,000</p>
      </div>
      <div className="product-card">
        <h3>商品B</h3>
        <p>2,000</p>
      </div>
    </div>
  );
}

// 分割後:ProductCardコンポーネントとして抽出
function ProductCard({ name, price }) {
  return (
    <div className="product-card">
      <h3>{name}</h3>
      <p>{price}</p>
    </div>
  );
}

function ProductList() {
  return (
    <div>
      <ProductCard name="商品A" price={1000} />
      <ProductCard name="商品B" price={2000} />
    </div>
  );
}

単一責任の原則

一つのコンポーネントが複数の責任を持っている場合、分割を検討します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 分割前:ヘッダーとコンテンツが混在
function Page() {
  return (
    <div>
      <header>
        <h1>サイトタイトル</h1>
        <nav>
          <a href="/">ホーム</a>
          <a href="/about">About</a>
        </nav>
      </header>
      <main>
        <article>
          <h2>記事タイトル</h2>
          <p>記事本文...</p>
        </article>
      </main>
    </div>
  );
}

// 分割後:責任ごとにコンポーネント化
function Header() {
  return (
    <header>
      <h1>サイトタイトル</h1>
      <Navigation />
    </header>
  );
}

function Navigation() {
  return (
    <nav>
      <a href="/">ホーム</a>
      <a href="/about">About</a>
    </nav>
  );
}

function Article({ title, content }) {
  return (
    <article>
      <h2>{title}</h2>
      <p>{content}</p>
    </article>
  );
}

function Page() {
  return (
    <div>
      <Header />
      <main>
        <Article title="記事タイトル" content="記事本文..." />
      </main>
    </div>
  );
}

複雑さの管理

1つのコンポーネントが大きくなりすぎた場合(目安として100行以上)、分割を検討します。

分割しすぎに注意

一方で、過度な分割は避けるべきです。以下の場合は分割しない方が良いでしょう。

  • 再利用の予定がない小さなUI部品
  • 分割することでかえって理解しにくくなる場合
  • Propsのバケツリレーが深くなりすぎる場合
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 過度な分割の例(避けるべき)
function Title({ text }) {
  return <h1>{text}</h1>;
}

function TitleText({ text }) {
  return <span>{text}</span>;
}

// シンプルに書く方が良い
function Header() {
  return <h1>サイトタイトル</h1>;
}

ファイル構成のベストプラクティス

プロジェクトが大きくなるにつれて、ファイル構成の重要性が増します。

1ファイル1コンポーネント

基本的に、1つのファイルに1つのコンポーネントを定義します。

src/
├── components/
│   ├── Button.jsx
│   ├── Card.jsx
│   └── Header.jsx
├── App.jsx
└── main.jsx

export/importの使い方

コンポーネントを別ファイルで使用するには、export/importを使用します。

名前付きエクスポート

1
2
3
4
5
6
7
// Button.jsx
export function Button({ children }) {
  return <button className="btn">{children}</button>;
}

// 使用側
import { Button } from './components/Button';

デフォルトエクスポート

1
2
3
4
5
6
7
8
9
// Button.jsx
function Button({ children }) {
  return <button className="btn">{children}</button>;
}

export default Button;

// 使用側
import Button from './components/Button';

どちらを使うかはプロジェクトの方針によりますが、1ファイル1コンポーネントの場合はデフォルトエクスポートが一般的です。

機能別のディレクトリ構成

中〜大規模プロジェクトでは、機能別にディレクトリを分けることがあります。

src/
├── components/
│   ├── common/           # 共通コンポーネント
│   │   ├── Button.jsx
│   │   ├── Card.jsx
│   │   └── Modal.jsx
│   ├── layout/           # レイアウトコンポーネント
│   │   ├── Header.jsx
│   │   ├── Footer.jsx
│   │   └── Sidebar.jsx
│   └── features/         # 機能別コンポーネント
│       ├── auth/
│       │   ├── LoginForm.jsx
│       │   └── SignupForm.jsx
│       └── products/
│           ├── ProductCard.jsx
│           └── ProductList.jsx
├── App.jsx
└── main.jsx

コンポーネントとスタイルのまとめ方

コンポーネントと関連ファイルをディレクトリにまとめるパターンもあります。

src/
├── components/
│   ├── Button/
│   │   ├── Button.jsx
│   │   ├── Button.css
│   │   ├── Button.test.jsx
│   │   └── index.js
│   └── Card/
│       ├── Card.jsx
│       ├── Card.css
│       └── index.js

index.jsを用意することで、インポート時のパスを短くできます。

1
2
3
4
5
// index.js
export { default } from './Button';

// 使用側
import Button from './components/Button';  // index.jsが自動で読み込まれる

実践的なコンポーネント設計

実際の開発で役立つコンポーネント設計パターンを紹介します。

汎用的なButtonコンポーネント

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function Button({ 
  children, 
  variant = 'primary', 
  size = 'medium', 
  disabled = false,
  onClick 
}) {
  const className = `btn btn-${variant} btn-${size}`;
  
  return (
    <button 
      className={className} 
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// 使用例
function App() {
  return (
    <div>
      <Button variant="primary" size="large" onClick={() => alert('送信')}>
        送信する
      </Button>
      <Button variant="secondary" size="small">
        キャンセル
      </Button>
      <Button variant="danger" disabled>
        削除
      </Button>
    </div>
  );
}

Cardコンポーネント

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Card({ title, children, footer }) {
  return (
    <div className="card">
      {title && (
        <div className="card-header">
          <h3>{title}</h3>
        </div>
      )}
      <div className="card-body">
        {children}
      </div>
      {footer && (
        <div className="card-footer">
          {footer}
        </div>
      )}
    </div>
  );
}

// 使用例
function App() {
  return (
    <Card 
      title="お知らせ" 
      footer={<Button>詳しく見る</Button>}
    >
      <p>新機能がリリースされました</p>
    </Card>
  );
}

リストとアイテムのパターン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function TodoItem({ todo, onToggle }) {
  return (
    <li className={todo.completed ? 'completed' : ''}>
      <input 
        type="checkbox" 
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
    </li>
  );
}

function TodoList({ todos, onToggle }) {
  if (todos.length === 0) {
    return <p>タスクがありません</p>;
  }
  
  return (
    <ul className="todo-list">
      {todos.map((todo) => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onToggle={onToggle}
        />
      ))}
    </ul>
  );
}

コンポーネントのデバッグ

開発中にコンポーネントの問題を特定する方法を紹介します。

React Developer Tools

ブラウザの拡張機能「React Developer Tools」を使用すると、コンポーネントツリーの確認やProps/Stateの監視ができます。

console.logによるデバッグ

コンポーネント内でconsole.logを使用して、Propsや変数の値を確認できます。

1
2
3
4
5
6
7
8
9
function UserCard({ user }) {
  console.log('UserCard rendered:', user);
  
  return (
    <div className="user-card">
      <h2>{user.name}</h2>
    </div>
  );
}

コンポーネントが再レンダリングされるタイミング

コンポーネントは以下の場合に再レンダリングされます。

  • Propsが変更された場合
  • State(状態)が変更された場合
  • 親コンポーネントが再レンダリングされた場合
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function Counter() {
  const [count, setCount] = useState(0);
  
  console.log('Counter rendered, count:', count);
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

ボタンをクリックするたびに、コンソールにログが出力されることを確認できます。

まとめ

本記事では、Reactの関数コンポーネントについて解説しました。

  • コンポーネントとは:UIを構成する独立した再利用可能なパーツ
  • 関数コンポーネント:JSXを返す関数として定義、通常の関数宣言またはアロー関数を使用
  • 命名規則:コンポーネント名は必ず大文字で始める
  • 分割の基準:再利用性、単一責任の原則、複雑さの管理を考慮
  • ファイル構成:1ファイル1コンポーネント、機能別のディレクトリ構成

適切なコンポーネント設計は、保守しやすく拡張しやすいアプリケーションの基盤となります。次の記事では、コンポーネント間でデータを受け渡すための「Props」について詳しく解説します。

参考リンク