はじめに#
前回の記事では、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が自動で読み込まれる
|
実践的なコンポーネント設計#
実際の開発で役立つコンポーネント設計パターンを紹介します。
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」を使用すると、コンポーネントツリーの確認や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」について詳しく解説します。
参考リンク#