デザインについての学習メモブログ

React入門 #05 – コンポーネントの基本(関数コンポーネントの作り方)

記事内に広告が含まれています。

React入門 #05 – コンポーネントの基本(関数コンポーネントの作り方)

コンポーネントは、Reactの核となる概念です。

UIを再利用可能な独立した部品に分割することで、保守性が高く、スケーラブルなアプリケーションを構築できます。

この記事では、関数コンポーネントの作り方と、効果的な設計方法を学んでいきます。

市販の入門書は、JavaScriptで書かれている書籍が多いため、TypeScriptを学び始めた方が「JavaScriptのコードをTypeScriptではどう書くのか?」を比較できるようにしました。

すでにTypeScriptに慣れている方は、TypeScript版のコードだけを参照してください。

コンポーネントとは?

まずは、Reactの最も重要な概念である「コンポーネント」について理解しましょう。

UIを小さな部品に分割することで、保守しやすく再利用可能なコードが書けるようになります。

コンポーネントの概念

コンポーネントは、UIの一部を表す独立した再利用可能な部品です。(車のハンドルやタイヤ、エンジンのような部品のイメージ)

レゴブロックのように、小さなパーツを組み合わせて大きなアプリケーションを作ります。

例:Webページの構成

Bash
Webページ
├── Header(ヘッダー)
   ├── Logo(ロゴ)
   └── Navigation(ナビゲーション)
├── Main(メインコンテンツ)
   ├── Article(記事)
      ├── Title(タイトル)
      └── Content(本文)
   └── Sidebar(サイドバー)
└── Footer(フッター)

コンポーネントのメリット

  • 再利用性 同じコンポーネントを複数の場所で使える
  • 保守性 各コンポーネントが独立しているため、修正が容易
  • テストのしやすさ 個別にテストできる
  • チーム開発 複数人で並行して開発しやすい

関数コンポーネントの基本

Reactでコンポーネントを作る方法はクラスコンポーネント(レガシー)もありますが、現在の主流は「関数コンポーネント」です。

シンプルで読みやすく、Hooksとの相性も良いため、これから学ぶならまず関数コンポーネントをマスターしましょう。

最もシンプルなコンポーネント

JavaScript版:

JSX
function Welcome() {
  return <h1>Welcome to React!</h1>
}

export default Welcome

TypeScript版:

TSX
function Welcome(): JSX.Element {
  return <h1>Welcome to React!</h1>
}

export default Welcome

コンポーネントの定義ルール:

  • 関数名は必ず大文字で始める(PascalCase)
  • JSXを返す(#4でも解説
  • exportして他のファイルから利用できるようにする

アロー関数での書き方

関数宣言アロー関数、どちらでも書けます。

関数宣言:

JSX
function Button() {
  return <button>クリック</button>
}

アロー関数

JSX
const Button = () => {
  return <button>クリック</button>
}

// 省略形(1行の場合)
const Button = () => <button>クリック</button>

TypeScript版(アロー関数):

TSX
const Button = (): JSX.Element => {
  return <button>クリック</button>
}

// または React.FC を使う方法(最近はあまり推奨されない)
const Button: React.FC = () => {
  return <button>クリック</button>
}

💡 どちらを使うべき?

  • チームの規約に従う
  • 個人的には関数宣言が読みやすいと感じる人が多い
  • ホイスティング(巻き上げ)の挙動が異なる

コンポーネントの使い方

作成したコンポーネントを実際に使ってみましょう。

他のコンポーネント内で呼び出したり、ネスト(入れ子)構造を作ったりすることで、複雑なUIを構築できます。

他のコンポーネントで使う

コンポーネントは`import`して使います。`export default`でエクスポートしたコンポーネントは、他のファイルから読み込めるようになります。

Button.jsx:

JSX
function Button() {
  return <button>クリック</button>
}

export default Button

App.jsx:

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版:

JSX
// 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 Header

TypeScript版:

TSX
// 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
TSX
//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回再利用しています。

TSX
// App.jsxまたはApp.tsx
import Greeting from './Greeting'

function App() {
  return (
    <div>
      <Greeting name="太郎" />
      <Greeting name="花子" />
      <Greeting name="次郎" />
    </div>
  )
}

JavaScript版:

JSX
// Greeting.jsx
function Greeting(props) {
  return <h1>こんにちは、{props.name}さん!</h1>
}

export default Greeting

TypeScript版:

TSX
// 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つの部分に分解できます: 段階的に見てみましょう:

TSX
// ステップ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版:

JSX
// ❌ 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版:

TSX
// 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版:

JSX
// 方法1: デフォルト引数
function Button({ text = 'クリック', color = 'blue' }) {
  return (
    <button style={{ backgroundColor: color }}>
      {text}
    </button>
  )
}

// 使い方</em>
<Button />  {/* デフォルト値が使われる */}
<Button text="送信" color="green" />

TypeScript版:

TypeScriptでは`?`が必要です。

`text?: string`の`?`は「省略可能」を意味する(オプショナル

TSX
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版:

JSX
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版:

TSX
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では、コードを書いている時点でエディタが間違いを教えてくれます。

TSX
// JavaScript: 実行するまでエラーに気づかない 
<ProductCard price="120000" /> 
// 💥 文字列を渡してしまった!(実行時エラー)


// TypeScript: エディタが即座にエラー表示 
<ProductCard price="120000" /> 
// ❌ エラー: 型 'string' を型 'number' に割り当てることはできません 

<ProductCard price={120000} /> 
// ✅ 正しい > > // 関数の引数の型も保証される

このように、TypeScriptは**バグを未然に防ぎ、開発体験を向上させます**

childrenプロパティ

`children`は特別なpropで、コンポーネントのタグの間に書かれた内容を受け取ります。

HTMLのように、開始タグと終了タグの間にコンテンツを配置できるようになり、より柔軟なコンポーネント設計が可能になります。

JavaScript版:

JSX
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版:

TSX
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がレンダリングできるあらゆるものを表すことができます。

TSX
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版:

JSX
// Button.jsx
function Button({ children, onClick, variant = 'primary' }) {
  const className = `btn btn-${variant}`
  
  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  )
}

export default Button

TypeScript版:

TSX
// 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
TSX
// 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版:

JSX
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版:

TSX
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版:

JSX
// 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版:

TSX
// 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のように、構造を見ただけで使い方が分かる
  • カスタマイズ性 – 順序を変えたり、一部を省略したりできる

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

プロジェクトが大きくなってきたら、ファイルやフォルダの整理が重要になります。

将来的にメンテナンスしやすい、スケーラブルなディレクトリ構成を学びましょう。

基本的なディレクトリ構成

プロジェクトが大きくなると、コンポーネントファイルをどう整理するかが重要になります。

適切なディレクトリ構成にすることで、ファイルを探しやすく、メンテナンスしやすいプロジェクトになります。

Bash
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

ディレクトリを分ける基準:

  1. 用途で分ける – 汎用コンポーネント(`common`)、レイアウト(`layout`)、機能別(`features`)
  2. 再利用性で分ける – どこでも使えるものと、特定機能専用のものを分離
  3. 関連性で分ける – Todo機能に関するコンポーネントは`features/todo/`にまとめる

ディレクトリ構成のメリット:

  • ファイルが探しやすい – 目的のコンポーネントがどこにあるか一目で分かる
  • 役割が明確 – コンポーネントの種類や目的がディレクトリ名から分かる
  • チーム開発がスムーズ – メンバー全員が同じルールでファイルを配置できる
  • スケーラブル – プロジェクトが大きくなっても混乱しない

コンポーネントごとにフォルダを作る

より大規模なプロジェクトでは、1つのコンポーネントに関連するファイル(スタイル、テスト、型定義など)をフォルダにまとめる方法が推奨されます。

関連ファイルが1箇所に集まることで、保守性が大幅に向上します。

より大規模なプロジェクト:

Bash
src/
├── components/
   └── Button/
       ├── Button.jsx          # コンポーネント本体
       ├── Button.css          # スタイル
       ├── Button.test.jsx     # テスト
       └── index.js            # エクスポート

index.jsの内容:

components/Button/index.jsは、index.jsを使うことで、ファイル名を省略できます。

JavaScript
export { default } from './Button'

これにより、以下のようにインポートできます:

JSX
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つの責任だけを持つべきです。

❌ 悪い例:

JSX
// 1つのコンポーネントで多くのことをしている
function UserDashboard() {
  // ユーザー情報の取得
  // 統計データの計算
  // グラフの描画
  // 通知の表示
  // プロフィール編集
  // ... 100行以上のコード
}

✅ 良い例:

JSX
function UserDashboard() {
  return (
    <div>
      <UserProfile />
      <UserStats />
      <ActivityGraph />
      <NotificationList />
    </div>
  )
}

// 各コンポーネントが独立して管理される

2. propsは読み取り専用

propsを直接変更してはいけません。

表示のためにの状態値イベント発生時に呼び出すハンドラーを渡すだけ

状態値を更新する処理親コンポーネントで実行

❌ 悪い例:

JSX
function Counter({ count }) {
  count = count + 1  // エラー!propsは読み取り専用
  return <div>{count}</div>
}

✅ 良い例:

JSX
function Counter({ count, onIncrement }) {
  return (
    <div>
      <span>{count}</span>
      <button onClick={onIncrement}>+1</button>
    </div>
  )
}
JSX
// 親コンポーネント: 状態を管理し、更新関数を渡す
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. コンポーネント名は説明的に

ページを構成する部品となるコンポーネントの作成にあたって、コンポーネントは、そのコンポーネントが何をするのか一目で分かるように命名しましょう。

略語や抽象的な名前は避け、具体的で説明的な名前を付けることで、コードの可読性が大幅に向上します。

❌ 悪い例:

JSX
function C1() { ... }
function Thing() { ... }
function Component() { ... }

✅ 良い例:

JSX
function UserProfileCard() { ... } // ユーザープロフィールのカードだと分かる
function ShoppingCartItem() { ... } // ショッピングカートのアイテムだと分かる
function NavigationMenu() { ... } // ナビゲーションメニューだと分かる
function ProductSearchBar() { ... }     // 商品検索バーだと分かる
function LoginForm() { ... }            // ログインフォームだと分かる

4. propsの数は適切に

propsが多すぎる場合は、オブジェクトにまとめるか、コンポーネントを分割します。

❌ 悪い例:

JSX
<UserCard
  firstName="太郎"
  lastName="山田"
  email="yamada@example.com"
  phone="090-1234-5678"
  address="東京都..."
  birthday="1990-01-01"
  job="エンジニア"
  company="ABC株式会社"
  // 10個以上のprops...
/>

✅ 良い例:

JSX
// オブジェクトにまとめる
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版:

JSX
// 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
JSX
// 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版:

TSX
// 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を作っていきましょう!

コンポーネントの設計は、経験を積むほど上手くなります。最初から完璧を目指さず、リファクタリングしながら改善していく姿勢が大切です!