Reactの基礎 - コンポーネント
概要
Reactにおけるコンポーネントは、UIを構成する独立した再利用可能な部品である。
ページ全体を1つの大きなHTML構造として記述するのではなく、ボタン、フォーム、ナビゲーションバー等を個別のコンポーネントとして定義し、それらを組み合わせてアプリケーションを構築する。
Reactコンポーネントは本質的にTypeScriptの関数であり、TSX (TypeScript + JSX) を返すことでUIを描画する。
React 16.8以降では、Hooks (useState、useEffect等) の登場により、関数コンポーネントがステート管理やライフサイクル処理に対応できるようになり、現在の標準的な記述スタイルとなっている。
コンポーネントベースのアーキテクチャには以下に示すメリットがある。
| 特性 | 説明 |
|---|---|
| 再利用性 | 同じコンポーネントを複数の箇所で使い回すことができる。 |
| 保守性 | 変更が必要な場合、対象のコンポーネントのみを修正すればよい。 |
| テスト容易性 | 独立した単位として個別にテストできる。 |
| 関心の分離 | UIのロジックと表示を局所化できる。 |
関数コンポーネントの定義
関数コンポーネントは、TSX (TypeScript + JSX) を返すTypeScript関数として定義する。
基本的な構文
関数コンポーネントを定義する方法は、function宣言とアロー関数の2種類がある。
どちらも同等に機能するため、プロジェクトの慣例に合わせて選択する。
- function宣言による定義
function Profile() { return ( <img src="https://example.com/avatar.jpg" alt="プロフィール画像" /> ); }
- アロー関数による定義
const Profile = () => { return ( <img src="https://example.com/avatar.jpg" alt="プロフィール画像" /> ); };
TSXを返す
コンポーネントは、return文でTSXを返す。
TSXの記述方法はシングルラインと複数行の2パターンがある。
TSXが単一行に収まる場合は、丸括弧なしで記述できる。
// 単一行の場合 : 丸括弧は不要
function Avatar() {
return <img src="https://example.com/avatar.jpg" alt="アバター" />;
}
TSXが複数行にわたる場合は、丸括弧で囲む必要がある。
丸括弧を省略すると、returnキーワードの後に改行がある場合にTypeScriptが自動的にセミコロンを挿入し、以降のTSXが無視されるため注意が必要である。
// 複数行の場合 : 丸括弧が必須
function UserCard() {
return (
<div className="card">
<h2>ユーザ名</h2>
<p>自己紹介テキスト</p>
</div>
);
}
アロー関数では、return文と丸括弧を省略した暗黙returnを使用することもできる。
// アロー関数の暗黙return
const Avatar = () => (
<img src="https://example.com/avatar.jpg" alt="アバター" />
);
コンポーネントの命名規則
Reactコンポーネントの命名には、厳守すべき規則が存在する。
PascalCase
Reactコンポーネントの名前は必ずパスカルケース (PascalCase, 各単語の先頭を大文字にする形式) で記述する。
この規則はReactの内部処理と直接関係している。
Reactは大文字で始まる識別子をコンポーネントとして扱い、小文字で始まる識別子をHTMLのネイティブタグとして扱う。
例えば、<Profile /> はReactコンポーネントとして認識されるが、<profile /> はHTMLタグとして処理される。
下表に、命名例を示す。
| 命名例 | 判定 | 説明 |
|---|---|---|
| function Profile() {} | 正しい | PascalCaseのため、コンポーネントとして認識される。 |
| function profile() {} | 誤り | 小文字始まりのため、HTMLタグとして認識される。 |
| function UserCard() {} | 正しい | 複数語もPascalCaseで結合する。 |
| function user_card() {} | 誤り | スネークケースは使用しない。 |
| <Profile /> | 正しい | Reactコンポーネントとして処理される。 |
| <profile /> | 誤り | HTMLタグとして処理される。 |
ファイル名の慣例
コンポーネントのファイル名は、コンポーネント名と一致させることが慣例である。
例えば、Profile コンポーネントは Profile.tsx というファイルに定義する。
ファイルの拡張子はプロジェクトの構成によって異なる。
| 拡張子 | 対象プロジェクト | 用途 |
|---|---|---|
| .jsx | JavaScriptプロジェクト | JSXを含むReactコンポーネント |
| .tsx | TypeScriptプロジェクト | 型チェック付きのReactコンポーネント |
| .js | JavaScriptプロジェクト | JSXを含まないロジック、ヘルパー関数 |
| .ts | TypeScriptプロジェクト | JSXを含まない型付きロジック、ヘルパー関数 |
ファイル分割
コンポーネントはファイルに分割して管理することにより、コードの保守性と再利用性が向上する。
1ファイル1コンポーネント
原則として、1つのファイルに1つのコンポーネントを定義する。
コンポーネントをファイルに定義したら、exportでエクスポートし、使用する側でimportして利用する。
Reactにはdefault exportとnamed exportの2種類のエクスポート方法がある。
// Profile.tsxファイル
// default export: 1ファイルにつき1つだけ使用できる
export default function Profile() {
return <img src="https://example.com/avatar.jpg" alt="プロフィール" />;
}
// App.tsxファイル
// default exportはimport時に任意の名前を付けられる
import ProfileComponent from './Profile';
function App() {
return <ProfileComponent />;
}
named exportを使用する場合は以下の通りである。
// components/buttons.tsxファイル
interface ButtonProps {
label: string;
}
// named export : 1ファイルに複数定義できる
export function PrimaryButton({ label }: ButtonProps) {
return <button className="btn-primary">{label}</button>;
}
export function SecondaryButton({ label }: ButtonProps) {
return <button className="btn-secondary">{label}</button>;
}
// App.tsxファイル
// named exportはimport時に波括弧で名前を一致させる必要がある
import { PrimaryButton, SecondaryButton } from './components/buttons';
ディレクトリ構成の例
プロジェクトの規模に応じて、コンポーネントのディレクトリ構成を選択する。
- 小規模プロジェクトの場合
- components ディレクトリにコンポーネントをまとめるシンプルな構成が適している。
src/ ├── App.tsx └── components/ ├── Button.tsx ├── Header.tsx └── Profile.tsx
- 中大規模プロジェクトの場合
- 機能別 (feature-based) のディレクトリ構成が保守性を高める。
src/ ├── features/ │ ├── user/ │ │ ├── components/ │ │ └── hooks/ │ └── product/ │ ├── components/ │ └── hooks/ └── components/ └── ui/ ├── Button.tsx └── Modal.tsx
コンポーネントにテストファイルやCSSモジュールを伴う場合は、コンポーネントフォルダを作成して関連ファイルをまとめる方法も有効である。
components/ └── Button/ ├── Button.tsx ├── Button.test.tsx └── Button.module.css
index.tsxによるバレルエクスポート
バレルエクスポートとは、index.tsx ファイルを使って複数のエクスポートを1箇所に集約するパターンである。
// components/index.tsx
export { Button } from './Button/Button';
export { Header } from './Header/Header';
export { Profile } from './Profile/Profile';
バレルエクスポートを使用すると、import文を短縮できる。
// バレルエクスポートなしの場合
import { Button } from './components/Button/Button';
import { Header } from './components/Header/Header';
// バレルエクスポートありの場合 (短縮される)
import { Button, Header } from './components';
バレルエクスポートを使用する場合は、以下の事柄に注意する。
- 円形参照のリスク
- モジュールが相互に参照し合うと循環依存が発生する場合がある。
- バンドルサイズへの影響
- ツールによっては、不要なコンポーネントもバンドルに含まれる可能性がある。
コンポーネントツリー
Reactアプリケーションは、コンポーネントを階層的に組み合わせたツリー構造として構成される。
ツリー構造の考え方
アプリケーションのルートには通常 App コンポーネントが配置され、そこから子コンポーネントが展開される。
コンポーネントツリーの例を以下に示す。
# コンポーネントツリーの例 App ├── Header │ ├── Logo │ └── Navigation ├── MainContent │ ├── Sidebar │ └── Feed │ └── Post │ ├── Author │ └── CommentList └── Footer
重要な注意事項として、コンポーネントの定義は必ずトップレベルで行う必要がある。
他のコンポーネント関数の内部にコンポーネントを定義することは禁止されている。
内部に定義すると、親コンポーネントが再レンダリングされるたびに子コンポーネントが再生成され、パフォーマンスの低下やステート管理のバグの原因となる。
// 正しい : トップレベルで定義する
function Profile() {
return <img src="..." />;
}
function Gallery() {
return <Profile />;
}
// 誤り : コンポーネント内にコンポーネントを定義してはいけない
function Gallery() {
function Profile() { // この定義は毎回再生成される
return <img src="..." />;
}
return <Profile />;
}
コンポーネントの合成 (Composition)
Reactではコンポーネントを組み合わせることにより、複雑なUIを構築する。
これをコンポーネントの合成 (Composition) と呼ぶ。
Propsを通じてデータを渡す合成の例を以下に示す。
interface UserCardProps {
name: string;
avatarUrl: string;
}
function UserCard({ name, avatarUrl }: UserCardProps) {
return (
<div className="card">
<Avatar url={avatarUrl} />
<h2>{name}</h2>
</div>
);
}
function App() {
return (
<UserCard
name="田中 太郎"
avatarUrl="https://example.com/avatar.jpg"
/>
);
}
childrenパターンを使用すると、JSX要素そのものを子として渡すことができる。
interface CardProps {
children: React.ReactNode;
}
// Cardコンポーネントはchildrenを受け取る汎用コンテナ
function Card({ children }: CardProps) {
return <div className="card-wrapper">{children}</div>;
}
function App() {
return (
<Card>
<h2>タイトル</h2>
<p>任意のコンテンツを渡せる</p>
</Card>
);
}
複数の子コンポーネントが同じ状態を共有する必要がある場合は、状態を共通の親コンポーネントに配置する。
これを「状態の持ち上げ (Lifting State Up)」と呼ぶ。
コンポーネント分割の指針
コンポーネントを適切な粒度で分割することは、保守性の高いソースコードを記述する上で重要である。
下表に、分割を検討すべき状況を示す。
| 基準 | 状況 |
|---|---|
| 単一責任の原則 | 1つのコンポーネントが複数の異なる役割を担っている場合 |
| コードの行数 | 1ファイルが150行から200行を超えてきた場合 |
| UIパターンの重複 | 同じUIパターンが複数箇所で使われている場合 |
| テストの複雑化 | コンポーネントのテストが書きにくく、複雑になっている場合 |
関数コンポーネントとクラスコンポーネント
Reactにはコンポーネントを定義する方法として、関数コンポーネントとクラスコンポーネントの2種類が存在する。
関数コンポーネント
関数コンポーネントはTypeScript関数としてコンポーネントを定義する形式であり、React 16.8以降のHooksの導入により、ステート管理やライフサイクル処理にも対応できるようになった。
現在のReact開発における標準的な記述スタイルである。
関数コンポーネントの記述例を以下に示す。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
増加
</button>
</div>
);
}
クラスコンポーネント
クラスコンポーネントはReact初期から存在するコンポーネントの記述形式である。
React 16.8でHooksが導入された以降は、新規開発では関数コンポーネントを使用することが推奨されている。
クラスコンポーネントはレガシーコードのメンテナンスや、一部の特殊なユースケース (Error Boundary等) においては引き続き使用される。
クラスコンポーネントの記述例を以下に示す。
import React from 'react';
interface CounterProps {}
interface CounterState {
count: number;
}
class Counter extends React.Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>カウント: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
増加
</button>
</div>
);
}
}
下表に、関数コンポーネントとクラスコンポーネントの比較を示す。
| 項目 | 関数コンポーネント | クラスコンポーネント |
|---|---|---|
| 構文 | function MyComponent() {} |
class MyComponent extends React.Component {}
|
| ステート管理 | useState Hook を使用 |
this.state を使用
|
| ライフサイクル | useEffect Hook を使用 |
componentDidMount 等のメソッドを使用
|
this の使用 |
不要 | 必要 (バインディングの問題が生じる場合がある) |
| 学習曲線 | 低い | 高い |
| 推奨度 | 推奨 (React 16.8以降の標準) | レガシー・メンテナンス用途 |
関連情報
- Reactの基礎 - TSXの基本構文
- Reactの基礎 - JSXの基本構文
- Reactの基礎 - 条件レンダリング
- Reactの基礎 - リストレンダリング
- Reactの基礎 - PropsとChildren