コンポーネントは、Reactの核となる概念です。
UIを再利用可能な独立した部品に分割することで、保守性が高く、スケーラブルなアプリケーションを構築できます。
この記事では、関数コンポーネントの作り方と、効果的な設計方法を学んでいきます。
市販の入門書は、JavaScriptで書かれている書籍が多いため、TypeScriptを学び始めた方が「JavaScriptのコードをTypeScriptではどう書くのか?」を比較できるようにしました。
すでにTypeScriptに慣れている方は、TypeScript版のコードだけを参照してください。
Contents
コンポーネントとは?
まずは、Reactの最も重要な概念である「コンポーネント」について理解しましょう。
UIを小さな部品に分割することで、保守しやすく再利用可能なコードが書けるようになります。
コンポーネントの概念
コンポーネントは、UIの一部を表す独立した再利用可能な部品です。(車のハンドルやタイヤ、エンジンのような部品のイメージ)
レゴブロックのように、小さなパーツを組み合わせて大きなアプリケーションを作ります。
例:Webページの構成
Webページ
├── Header(ヘッダー)
│ ├── Logo(ロゴ)
│ └── Navigation(ナビゲーション)
├── Main(メインコンテンツ)
│ ├── Article(記事)
│ │ ├── Title(タイトル)
│ │ └── Content(本文)
│ └── Sidebar(サイドバー)
└── Footer(フッター)コンポーネントのメリット
- 再利用性 同じコンポーネントを複数の場所で使える
- 保守性 各コンポーネントが独立しているため、修正が容易
- テストのしやすさ 個別にテストできる
- チーム開発 複数人で並行して開発しやすい
関数コンポーネントの基本
Reactでコンポーネントを作る方法はクラスコンポーネント(レガシー)もありますが、現在の主流は「関数コンポーネント」です。
シンプルで読みやすく、Hooksとの相性も良いため、これから学ぶならまず関数コンポーネントをマスターしましょう。
最もシンプルなコンポーネント
JavaScript版:
function Welcome() {
return <h1>Welcome to React!</h1>
}
export default WelcomeTypeScript版:
function Welcome(): JSX.Element {
return <h1>Welcome to React!</h1>
}
export default Welcomeコンポーネントの定義ルール:
- 関数名は必ず大文字で始める(PascalCase)
- JSXを返す(#4でも解説)
exportして他のファイルから利用できるようにする
アロー関数での書き方
関数宣言とアロー関数、どちらでも書けます。
関数宣言:
function Button() {
return <button>クリック</button>
}アロー関数:
const Button = () => {
return <button>クリック</button>
}
// 省略形(1行の場合)
const Button = () => <button>クリック</button>TypeScript版(アロー関数):
const Button = (): JSX.Element => {
return <button>クリック</button>
}
// または React.FC を使う方法(最近はあまり推奨されない)
const Button: React.FC = () => {
return <button>クリック</button>
}💡 どちらを使うべき?
- チームの規約に従う
- 個人的には関数宣言が読みやすいと感じる人が多い
- ホイスティング(巻き上げ)の挙動が異なる
コンポーネントの使い方
作成したコンポーネントを実際に使ってみましょう。
他のコンポーネント内で呼び出したり、ネスト(入れ子)構造を作ったりすることで、複雑なUIを構築できます。
他のコンポーネントで使う
コンポーネントは`import`して使います。`export default`でエクスポートしたコンポーネントは、他のファイルから読み込めるようになります。
Button.jsx:
function Button() {
return <button>クリック</button>
}
export default ButtonApp.jsx:
import Button from './Button'
function App() {
return (
<div>
<h1>マイアプリ</h1>
<Button />
<Button />
<Button />
</div>
)
}
export default App📝 補足:TypeScriptプロジェクトでも`.jsx`ファイルは動作します
TypeScriptプロジェクト(Vite + TypeScriptなど)で作成した場合でも、`.jsx`拡張子のファイルはそのまま動作します。
型チェックは行われませんが、JavaScriptとして正常に実行されます。 型安全性を得たい場合は、ファイル名を`.tsx`に変更し、型定義を追加してください(後述のTypeScript版を参照)。
ただし、App.tsxが元からあるのでApp.jsxはコードだけ、元のコードと差し替えて動作確認してください。
コンポーネントのネスト
コンポーネントの中で他のコンポーネントを使うことを「ネスト(入れ子)」といいます。
レゴブロックを組み合わせるように、小さなコンポーネントを組み合わせて大きなコンポーネントを作ります。
以下の例では、`Header`コンポーネントが`Logo`と`Navigation`という2つの子コンポーネントを含んでいます。
`export default`するのは`Header`のみ(`Logo`と`Navigation`はこのファイル内でのみ使用)
JavaScript版:
// Header.jsx
function Logo() {
return <div className="logo">MyApp</div>
}
function Navigation() {
return (
<nav>
<a href="/">ホーム</a>
<a href="/about">概要</a>
<a href="/contact">お問い合わせ</a>
</nav>
)
}
function Header() {
return (
<header>
<Logo />
<Navigation />
</header>
)
}
export default HeaderTypeScript版:
// Header.tsx
import { type JSX } from 'react'
// ^^^^
// これは「型としてのみインポート」を明示
function Logo(): JSX.Element {
return <div className="logo">MyApp</div>
}
function Navigation(): JSX.Element {
return (
<nav>
<a href="/">ホーム</a>
<a href="/about">概要</a>
<a href="/contact">お問い合わせ</a>
</nav>
)
}
function Header(): JSX.Element {
return (
<header>
<Logo />
<Navigation />
</header>
)
}
export default Header//App.tsx
import './App.css'
import Header from './Header'
import Button from './Button'
function App() {
return (
<>
<Header />
<Button />
</>
)
}
export default App実行結果を確認してみください。(npm run dev)
propsでデータを受け取る
コンポーネントを再利用可能にするための鍵が「props」です。
親コンポーネントから子コンポーネントへデータを渡すことで、同じコンポーネントを異なる内容で表示できるようになります。
propsの基本
props(properties)は、親コンポーネントから子コンポーネントへデータを渡す仕組みです。
これにより、同じコンポーネントを異なるデータで再利用できるようになります。
以下の例では、`Greeting`コンポーネントに異なる名前を渡して、3回再利用しています。
// App.jsxまたはApp.tsx
import Greeting from './Greeting'
function App() {
return (
<div>
<Greeting name="太郎" />
<Greeting name="花子" />
<Greeting name="次郎" />
</div>
)
}JavaScript版:
// Greeting.jsx
function Greeting(props) {
return <h1>こんにちは、{props.name}さん!</h1>
}
export default Greeting
TypeScript版:
// Greeting.tsx
import { type JSX } from 'react'
interface GreetingProps {
name: string
}
function Greeting(props: GreetingProps): JSX.Element {
return <h1>こんにちは、{props.name}さん!</h1>
}
export default Greeting
// または
//型を直接指定
function Greeting({ name }: { name: string }): JSX.Element {
return <h1>こんにちは、{name}さん!</h1>
}📝 `{ name }: { name: string }` の意味:
この書き方は、分割代入と型指定を同時に行っています。
少し複雑に見えますが、2つの部分に分解できます: 段階的に見てみましょう:
// ステップ1: 分割代入なし:最も基本的な書き方(初心者向け)
function Greeting(props: GreetingProps): JSX.Element {
return こんにちは、{props.name}さん!
}
// ステップ2: 分割代入を使う(型は別で定義)
function Greeting({ name }: GreetingProps): JSX.Element {
return こんにちは、{name}さん!
}
// ステップ3: 型を直接指定(interfaceを使わない)
function Greeting({ name }: { name: string }): JSX.Element {
return こんにちは、{name}さん!
} どれを使うべき
- ステップ2:`props.name`と毎回書かなくて良いので、コードがスッキリする
- ステップ3:interfaceを別に定義しなくて良いので、コードが短くなる
⚠️ GreetingPropsにプロパティをname以外に増やしたりするとステップ3の引数の型とは一致しなくなるのでエラーになります。データの構造が同じだから引数として渡せると考えてください。
💡 重要:「props」は慣習的な変数名
関数の引数名は`props`でなくても動作します。
例えば`function Greeting(data)`や`function Greeting(p)`でも問題ありません。
ただし、Reactコミュニティでは慣習的に`props`という名前を使います。
他の開発者が読んだときに「これはpropsだ」とすぐに分かるため、特別な理由がない限り`props`という名前を使いましょう。
分割代入(デストラクチャリング)
propsはオブジェクトなので、分割代入を使うとスッキリ書けます。
`props.name`、`props.age`と毎回`props.`を書くのは面倒ですし、propsが増えるほどコードが読みにくくなります。
JavaScript版:
// ❌ propsを毎回書く
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>年齢: {props.age}</p>
<p>職業: {props.job}</p>
</div>
)
}
// ✅ 分割代入を使う
function UserCard({ name, age, job }) {
return (
<div>
<h2>{name}</h2>
<p>年齢: {age}</p>
<p>職業: {job}</p>
</div>
)
}
// 使い方
<UserCard name="山田太郎" age={30} job="エンジニア" />TypeScript版:
// types.ts(型定義を別ファイルに)
export interface UserCardProps {
name: string
age: number
job: string
}
// UserCard.tsx
import { UserCardProps } from './types'
function UserCard({ name, age, job }: UserCardProps): JSX.Element {
return (
<div>
<h2>{name}</h2>
<p>年齢: {age}</p>
<p>職業: {job}</p>
</div>
)
}
export default UserCardデフォルト値の設定
propsは必須ではなく、省略可能にすることもできます。
propsが渡されなかった場合のデフォルト値を設定しておくことで、コンポーネントがより柔軟に使えるようになります。
デフォルト値を設定するメリット:
- propsを毎回指定しなくて良い(よく使う値をデフォルトにできる)
- コンポーネントの使い方がシンプルになる
- 省略時の挙動が明確になる
JavaScript版:
// 方法1: デフォルト引数
function Button({ text = 'クリック', color = 'blue' }) {
return (
<button style={{ backgroundColor: color }}>
{text}
</button>
)
}
// 使い方</em>
<Button /> {/* デフォルト値が使われる */}
<Button text="送信" color="green" />TypeScript版:
TypeScriptでは`?`が必要です。
`text?: string`の`?`は「省略可能」を意味する(オプショナル)
interface ButtonProps {
text?: string // ?は省略可能を意味する
color?: string
}
function Button({
text = 'クリック',
color = 'blue'
}: ButtonProps): JSX.Element {
return (
<button style={{ backgroundColor: color }}>
{text}
</button>
)
}様々なデータ型のprops
propsには文字列だけでなく、数値、真偽値、配列、関数など、あらゆるJavaScriptのデータ型を渡すことができます。
これにより、複雑なデータや処理を子コンポーネントに渡せるようになります。
JavaScript版:
function ProductCard({
name, // 文字列
price, // 数値
isAvailable, // 真偽値
tags, // 配列
onBuy // 関数
}) {
return (
<div className="product-card">
<h3>{name}</h3>
<p>価格: ¥{price.toLocaleString()}</p>
<p>在庫: {isAvailable ? 'あり' : 'なし'}</p>
<div>
{tags.map((tag, index) => (
<span key={index} className="tag">{tag}</span>
))}
</div>
<button onClick={onBuy} disabled={!isAvailable}>
購入する
</button>
</div>
)
}
// 使い方
function App() {
const handleBuy = () => {
alert('購入しました!')
}
return (
<ProductCard
name="ノートPC"
price={120000}
isAvailable={true}
tags={['電子機器', '人気', '新品']}
onBuy={handleBuy}
/>
)
}💡 重要:文字列以外は必ず波括弧`{}`で囲む
<ProductCard name=”ノートPC” />
{/* ✅ 文字列は “” */} >
<ProductCard price={120000} />
{/* ✅ 数値は {} */} >
<ProductCard price=”120000″ />
{/* ❌ 文字列になってしまう */} >
TypeScript版:
interface ProductCardProps {
name: string
price: number
isAvailable: boolean
tags: string[]
onBuy: () => void
}
function ProductCard({
name,
price,
isAvailable,
tags,
onBuy
}: ProductCardProps): JSX.Element {
return (
<div className="product-card">
<h3>{name}</h3>
<p>価格: ¥{price.toLocaleString()}</p>
<p>在庫: {isAvailable ? 'あり' : 'なし'}</p>
<div>
{tags.map((tag, index) => (
<span key={index} className="tag">{tag}</span>
))}
</div>
<button onClick={onBuy} disabled={!isAvailable}>
購入する
</button>
</div>
)
}💡 TypeScriptの恩恵:型安全性*
JavaScriptでは、間違った型のデータを渡してもエディタ上ではエラーが出ず、実行時に初めて問題が分かります。
TypeScriptでは、コードを書いている時点でエディタが間違いを教えてくれます。
// JavaScript: 実行するまでエラーに気づかない
<ProductCard price="120000" />
// 💥 文字列を渡してしまった!(実行時エラー)
// TypeScript: エディタが即座にエラー表示
<ProductCard price="120000" />
// ❌ エラー: 型 'string' を型 'number' に割り当てることはできません
<ProductCard price={120000} />
// ✅ 正しい > > // 関数の引数の型も保証される
このように、TypeScriptは**バグを未然に防ぎ、開発体験を向上させます**。
childrenプロパティ
`children`は特別なpropで、コンポーネントのタグの間に書かれた内容を受け取ります。
HTMLのように、開始タグと終了タグの間にコンテンツを配置できるようになり、より柔軟なコンポーネント設計が可能になります。
JavaScript版:
function Card({ children }) {
return (
<div className="card">
{children}
</div>
)
}
// 使い方
function App() {
return (
<Card>
<h2>カードのタイトル</h2>
<p>カードの内容です</p>
<button>アクション</button>
</Card>
)
}💡 childrenと通常のpropsの違い
- props: 属性として渡す
- children: タグの間に書く(より直感的で柔軟)
TypeScript版:
interface CardProps {
children: React.ReactNode
}
function Card({ children }: CardProps): JSX.Element {
return (
<div className="card">
{children}
</div>
)
}
// さらに他のpropsと組み合わせる
interface CardProps {
title?: string
children: React.ReactNode
}
// import { type ReactNode } from 'react'のようにインポートしておけば
// children: ReactNodeとスッキリ書けます
function Card({ title, children }: CardProps): JSX.Element {
return (
<div className="card">
{title && <h2>{title}</h2>}
<div className="card-content">
{children}
</div>
</div>
)
}
// 使い方
<Card title="お知らせ">
<p>新機能を追加しました!</p>
</Card>React.ReactNodeの型
Reactがレンダリングできるあらゆるものを表すことができます。
React.ReactNode(最も広い)
├── JSX.Element
├── string
├── number
├── boolean
├── null
├── undefined
└── React.ReactNode[](配列)
// これら全て有効:
<Card>こんにちは</Card> {/* 文字列 */}
<Card>{123}</Card> {/* 数値 */}
<Card><div>要素</div></Card> {/* JSX要素 */}
<Card>{null}</Card> {/* null */}
<Card>
<p>複数</p>
<p>の要素</p>
</Card> {/* 複数の要素(配列) */}
コンポーネントの設計パターン
実務では、コンポーネントをどう分割し、どう組み合わせるかが重要です。
ここでは、よく使われる3つの設計パターンを学び、保守性の高いコードを書けるようになりましょう。
- プレゼンテーショナルコンポーネント
- コンテナコンポーネント
- 合成コンポーネント
プレゼンテーショナルコンポーネント
見た目だけを担当するコンポーネントです。
データの取得や状態管理は行わず、propsで受け取ったデータを表示することに専念します。
JavaScript版:
// Button.jsx
function Button({ children, onClick, variant = 'primary' }) {
const className = `btn btn-${variant}`
return (
<button className={className} onClick={onClick}>
{children}
</button>
)
}
export default ButtonTypeScript版:
// Button.tsx
import { type JSX } from 'react'
interface ButtonProps {
children: React.ReactNode
onClick?: () => void
variant?: 'primary' | 'secondary' | 'danger'
}
function Button({
children,
onClick,
variant = 'primary'
}: ButtonProps): JSX.Element {
const className = `btn btn-${variant}`
return (
<button className={className} onClick={onClick}>
{children}
</button>
)
}
export default Button// App.tsx
import Button from './Button'
function App(): JSX.Element {
const handleSave = (): void => {
console.log('保存しました')
}
const handleDelete = (): void => {
if (confirm('本当に削除しますか?')) {
console.log('削除しました')
}
}
const handleCancel = (): void => {
console.log('キャンセルしました')
}
return (
<div style={{ padding: '20px' }}>
<h1>ユーザー編集画面</h1>
<div style={{ marginBottom: '20px' }}>
<h2>山田太郎</h2>
<p>メールアドレス: yamada@example.com</p>
<p>職業: エンジニア</p>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<Button variant="primary" onClick={handleSave}>
💾 保存
</Button>
<Button variant="secondary" onClick={handleCancel}>
↩️ キャンセル
</Button>
<Button variant="danger" onClick={handleDelete}>
🗑️ 削除
</Button>
</div>
</div>
)
}
export default App📝 このパターンを使うメリット:
- 再利用性が高い – 様々な場所で異なるデータで使える
- テストしやすい – propsを渡すだけでテストできる
- デザインの変更が容易 – 見た目の修正が他のロジックに影響しない
- 責任が明確 – 「見た目」だけに集中できる
コンテナコンポーネント
データの取得や状態管理を担当するコンポーネントです。
プレゼンテーショナルコンポーネントとは対照的に、「ロジック」に専念し、見た目はプレゼンテーショナルコンポーネントに任せます。
JavaScript版:
import { useState } from 'react'
import TodoList from './TodoList'
function TodoContainer() {
const [todos, setTodos] = useState([
{ id: 1, text: '買い物', completed: false },
{ id: 2, text: '洗濯', completed: true }
])
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false
}
setTodos([...todos, newTodo])
}
return (
<TodoList
todos={todos}
onToggle={toggleTodo}
onAdd={addTodo}
/>
)
}TypeScript版:
import { useState } from 'react'
import TodoList from './TodoList'
interface Todo {
id: number
text: string
completed: boolean
}
function TodoContainer(): JSX.Element {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: '買い物', completed: false },
{ id: 2, text: '洗濯', completed: true }
])
const toggleTodo = (id: number): void => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
const addTodo = (text: string): void => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
}
setTodos([...todos, newTodo])
}
return (
<TodoList
todos={todos}
onToggle={toggleTodo}
onAdd={addTodo}
/>
)
}TodoList.tsxは割愛していますが、プレゼンテーショナルコンポーネントであるTodoListをインポートして、状態やロジックをTodoListに渡しています。
プレゼンテーショナルコンポーネントとの役割分担:
┌──────────────────┐
│ コンテナコンポーネント │
│ (TodoContainer) │
│ ・useState でデータ管理 │
│ ・toggleTodo などのロジック │
│ ・見た目は持たない │
└──────────┬───────┘
│ propsでデータと関数を渡す
↓
┌───────────────────┐
│ プレゼンテーショナルコンポーネント │
│ (TodoList) │
│ ・受け取ったデータを表示 │
│ ・ボタンクリック時に関数を実行 │
│ ・ロジックは持たない │
└───────────────────┘このパターンを使うメリット:
- 責任の分離 – 「ロジック(データ管理)」と「見た目(UI)」を分離できる
- 再利用性 – 同じロジックで異なる見た目のUIを作れる
- テストしやすい – ロジックと見た目を別々にテストできる
- 保守性の向上 – データ構造の変更が見た目に影響しにくい
合成コンポーネント
複数の小さなコンポーネントを組み合わせて、柔軟で使いやすいコンポーネントを作るパターンです。
親コンポーネントにサブコンポーネントを紐付けることで、関連するコンポーネントをグループ化し、`Alert.Icon`や`Alert.Title`のように直感的に使えるようになります。
必要な部品だけを組み合わせて使える(Icon不要なら省略可能など)
この設計パターンは、React公式ライブラリやUIライブラリでも広く採用されており、柔軟で保守性の高いコンポーネントを作る際の標準的な手法です。
JavaScript版:
// Alert.jsx
function AlertIcon({ type }) {
const icons = {
success: '✓',
error: '✗',
warning: '⚠',
info: 'ℹ'
}
return <span className="alert-icon">{icons[type]}</span>
}
function AlertTitle({ children }) {
return <h4 className="alert-title">{children}</h4>
}
function AlertMessage({ children }) {
return <p className="alert-message">{children}</p>
}
function Alert({ type = 'info', children }) {
return (
<div className={`alert alert-${type}`}>
{children}
</div>
)
}
// サブコンポーネントを公開
Alert.Icon = AlertIcon
Alert.Title = AlertTitle
Alert.Message = AlertMessage
export default Alert
// 使い方
function App() {
return (
<Alert type="success">
<Alert.Icon type="success" />
<Alert.Title>成功しました!</Alert.Title>
<Alert.Message>データが保存されました。</Alert.Message>
</Alert>
)
}サンプルでは、Appコンポーネント内でAlertの内側を好みのパーツで組み合わせて、合成のコンポーネントを生成しています。
TypeScript版:
// Alert.tsx
interface AlertIconProps {
type: 'success' | 'error' | 'warning' | 'info'
}
function AlertIcon({ type }: AlertIconProps): JSX.Element {
const icons = {
success: '✓',
error: '✗',
warning: '⚠',
info: 'ℹ'
}
return <span className="alert-icon">{icons[type]}</span>
}
interface AlertChildProps {
children: React.ReactNode
}
function AlertTitle({ children }: AlertChildProps): JSX.Element {
return <h4 className="alert-title">{children}</h4>
}
function AlertMessage({ children }: AlertChildProps): JSX.Element {
return <p className="alert-message">{children}</p>
}
interface AlertProps {
type?: 'success' | 'error' | 'warning' | 'info'
children: React.ReactNode
}
function Alert({ type = 'info', children }: AlertProps): JSX.Element {
return (
<div className={`alert alert-${type}`}>
{children}
</div>
)
}
Alert.Icon = AlertIcon
Alert.Title = AlertTitle
Alert.Message = AlertMessage
export default Alert
// 使い方
function App() {
return (
<Alert type="success">
<Alert.Icon type="success" />
<Alert.Title>成功しました!</Alert.Title>
<Alert.Message>データが保存されました。</Alert.Message>
</Alert>
)
}📝 このパターンを使うメリット:
- 柔軟性– 必要な部品だけを組み合わせて使える(Icon不要なら省略可能)
- 名前空間の整理 – `Alert.Icon`のように、関連するコンポーネントがまとまる
- 直感的なAPI – HTMLのように、構造を見ただけで使い方が分かる
- カスタマイズ性 – 順序を変えたり、一部を省略したりできる
ファイル構成のベストプラクティス
プロジェクトが大きくなってきたら、ファイルやフォルダの整理が重要になります。
将来的にメンテナンスしやすい、スケーラブルなディレクトリ構成を学びましょう。
基本的なディレクトリ構成
プロジェクトが大きくなると、コンポーネントファイルをどう整理するかが重要になります。
適切なディレクトリ構成にすることで、ファイルを探しやすく、メンテナンスしやすいプロジェクトになります。
src/
├── components/
│ ├── common/ # 汎用コンポーネント
│ │ ├── Button.jsx
│ │ ├── Input.jsx
│ │ └── Card.jsx
│ ├── layout/ # レイアウトコンポーネント
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ └── Sidebar.jsx
│ └── features/ # 機能別コンポーネント
│ ├── todo/
│ │ ├── TodoList.jsx
│ │ ├── TodoItem.jsx
│ │ └── TodoForm.jsx
│ └── user/
│ ├── UserProfile.jsx
│ └── UserCard.jsx
├── App.jsx
└── main.jsxディレクトリを分ける基準:
- 用途で分ける – 汎用コンポーネント(`common`)、レイアウト(`layout`)、機能別(`features`)
- 再利用性で分ける – どこでも使えるものと、特定機能専用のものを分離
- 関連性で分ける – Todo機能に関するコンポーネントは`features/todo/`にまとめる
ディレクトリ構成のメリット:
- ファイルが探しやすい – 目的のコンポーネントがどこにあるか一目で分かる
- 役割が明確 – コンポーネントの種類や目的がディレクトリ名から分かる
- チーム開発がスムーズ – メンバー全員が同じルールでファイルを配置できる
- スケーラブル – プロジェクトが大きくなっても混乱しない
コンポーネントごとにフォルダを作る
より大規模なプロジェクトでは、1つのコンポーネントに関連するファイル(スタイル、テスト、型定義など)をフォルダにまとめる方法が推奨されます。
関連ファイルが1箇所に集まることで、保守性が大幅に向上します。
より大規模なプロジェクト:
src/
├── components/
│ └── Button/
│ ├── Button.jsx # コンポーネント本体
│ ├── Button.css # スタイル
│ ├── Button.test.jsx # テスト
│ └── index.js # エクスポートindex.jsの内容:
components/Button/index.jsは、index.jsを使うことで、ファイル名を省略できます。
export { default } from './Button'これにより、以下のようにインポートできます:
import Button from './components/Button'
// import Button from './components/Button/Button' ではなく
// Button だけで済む
このパターンを使うメリット:
- 関連ファイルが一箇所に – コンポーネント本体、スタイル、テストが同じフォルダ内
- 修正が容易 – ボタンを修正する際、Buttonフォルダだけ見れば良い
- 削除が安全 – コンポーネントを削除する際、フォルダごと削除すれば完了
- importパスがスッキリ – `index.js`を使うことで、フォルダ名だけでimportできる
基本構成との違い:
// 基本構成: ファイルが散らばる
components/
├── Button.jsx
├── Button.css
├── Button.test.jsx
├── Card.jsx
├── Card.css
└── Card.test.jsx
// フォルダ構成: 関連ファイルをまとめる
components/
├── Button/
│ ├── Button.jsx
│ ├── Button.css
│ ├── Button.test.jsx
│ └── index.js
└── Card/
├── Card.jsx
├── Card.css
├── Card.test.jsx
└── index.js コンポーネント設計のベストプラクティス
良いコンポーネントを作るためには、いくつかの原則を守ることが大切です。
ここでは、多くの開発者が実践している4つの重要な原則を紹介します。
- 単一責任の原則
- propsは読み取り専用
- コンポーネント名は説明的に
- propsの数は適切に
1. 単一責任の原則
1つのコンポーネントは1つの責任だけを持つべきです。
❌ 悪い例:
// 1つのコンポーネントで多くのことをしている
function UserDashboard() {
// ユーザー情報の取得
// 統計データの計算
// グラフの描画
// 通知の表示
// プロフィール編集
// ... 100行以上のコード
}✅ 良い例:
function UserDashboard() {
return (
<div>
<UserProfile />
<UserStats />
<ActivityGraph />
<NotificationList />
</div>
)
}
// 各コンポーネントが独立して管理される2. propsは読み取り専用
propsを直接変更してはいけません。
表示のためにの状態値とイベント発生時に呼び出すハンドラーを渡すだけ
状態値を更新する処理は親コンポーネントで実行
❌ 悪い例:
function Counter({ count }) {
count = count + 1 // エラー!propsは読み取り専用
return <div>{count}</div>
}✅ 良い例:
function Counter({ count, onIncrement }) {
return (
<div>
<span>{count}</span>
<button onClick={onIncrement}>+1</button>
</div>
)
}// 親コンポーネント: 状態を管理し、更新関数を渡す
function App() {
const [count, setCount] = useState(0)
const handleIncrement = () => {
setCount(count + 1) // 親で状態を更新
}
return (
<Counter
count={count} // 現在の値を渡す
onIncrement={handleIncrement} // 更新関数を渡す
/>
)
}const [count, setCount] = useState(0)について
状態管理(useState)を使った方法については、#7で解説する予定です。
現時点では、”このパターンで変数(コンポーネントの状態)の値を更新するんだな”といった感じでザックリ捉えておくだけで十分です。
setXxxxメソッドで状態を更新するルールです。
3. コンポーネント名は説明的に
ページを構成する部品となるコンポーネントの作成にあたって、コンポーネント名は、そのコンポーネントが何をするのか一目で分かるように命名しましょう。
略語や抽象的な名前は避け、具体的で説明的な名前を付けることで、コードの可読性が大幅に向上します。
❌ 悪い例:
function C1() { ... }
function Thing() { ... }
function Component() { ... }✅ 良い例:
function UserProfileCard() { ... } // ユーザープロフィールのカードだと分かる
function ShoppingCartItem() { ... } // ショッピングカートのアイテムだと分かる
function NavigationMenu() { ... } // ナビゲーションメニューだと分かる
function ProductSearchBar() { ... } // 商品検索バーだと分かる
function LoginForm() { ... } // ログインフォームだと分かる4. propsの数は適切に
propsが多すぎる場合は、オブジェクトにまとめるか、コンポーネントを分割します。
❌ 悪い例:
<UserCard
firstName="太郎"
lastName="山田"
email="yamada@example.com"
phone="090-1234-5678"
address="東京都..."
birthday="1990-01-01"
job="エンジニア"
company="ABC株式会社"
// 10個以上のprops...
/>✅ 良い例:
// オブジェクトにまとめる
const user = {
name: { first: '太郎', last: '山田' },
contact: { email: 'yamada@example.com', phone: '090-1234-5678' },
profile: { birthday: '1990-01-01', job: 'エンジニア' }
}
<UserCard user={user} />実践:ブログカードコンポーネント
これまで学んだ知識を総動員して、実践的なブログカードコンポーネントを作ってみましょう。
props、children、イベントハンドリングなど、様々な要素を組み合わせた実用的な例です。
完全な例として、ブログカードを作ってみましょう。
JavaScript版:
// BlogCard.jsx
function BlogCard({ title, excerpt, author, date, tags, imageUrl, onRead }) {
return (
<article className="blog-card">
<div className="blog-card-image">
<img src={imageUrl} alt={title} />
</div>
<div className="blog-card-content">
<div className="blog-card-tags">
{tags.map((tag, index) => (
<span key={index} className="tag">{tag}</span>
))}
</div>
<h2 className="blog-card-title">{title}</h2>
<p className="blog-card-excerpt">{excerpt}</p>
<div className="blog-card-meta">
<span className="author">{author}</span>
<span className="date">{date}</span>
</div>
<button onClick={onRead} className="read-more">
続きを読む →
</button>
</div>
</article>
)
}
export default BlogCard// App.jsx での使用例
function App() {
const handleRead = () => {
console.log('記事を開く')
}
return (
<BlogCard
title="React入門:コンポーネントの基本"
excerpt="Reactのコンポーネントについて、基礎から実践的な使い方まで解説します。"
author="山田太郎"
date="2025-01-15"
tags={['React', '入門', 'JavaScript']}
imageUrl="https://via.placeholder.com/400x200"
onRead={handleRead}
/>
)
}TypeScript版:
// types.ts
export interface BlogCardProps {
title: string
excerpt: string
author: string
date: string
tags: string[]
imageUrl: string
onRead: () => void
}
// BlogCard.tsx
import { BlogCardProps } from './types'
function BlogCard({
title,
excerpt,
author,
date,
tags,
imageUrl,
onRead
}: BlogCardProps): JSX.Element {
return (
<article className="blog-card">
<div className="blog-card-image">
<img src={imageUrl} alt={title} />
</div>
<div className="blog-card-content">
<div className="blog-card-tags">
{tags.map((tag, index) => (
<span key={index} className="tag">{tag}</span>
))}
</div>
<h2 className="blog-card-title">{title}</h2>
<p className="blog-card-excerpt">{excerpt}</p>
<div className="blog-card-meta">
<span className="author">{author}</span>
<span className="date">{date}</span>
</div>
<button onClick={onRead} className="read-more">
続きを読む →
</button>
</div>
</article>
)
}
export default BlogCard
// App.tsx は、jsx版サンプル下部の App.jsxの内容をそのまま貼り付けて動作チェックしてください。
// !!! import BlogCard from './BlogCard'も忘れずに!まとめ
この記事では、Reactコンポーネントの基本を詳しく学びました。
重要なポイント:
- コンポーネントは再利用可能なUI部品
- 関数コンポーネントが現在の標準
- propsでデータを親から子へ渡す
- childrenで柔軟なコンポーネント設計
- 単一責任の原則を守る
- TypeScriptで型安全性を高める
コンポーネント設計の原則:
- 小さく、焦点を絞ったコンポーネントを作る
- propsは読み取り専用
- 説明的な名前を付ける
- 適切な粒度で分割する
次のステップ: 次回は、useStateを使った状態管理について学びます。これまで学んだコンポーネントの知識を活かして、動的でインタラクティブなUIを作っていきましょう!
コンポーネントの設計は、経験を積むほど上手くなります。最初から完璧を目指さず、リファクタリングしながら改善していく姿勢が大切です!


























