コンポーネントの分割 — Props とイベントコールバック

コンポーネント化は React の最も中核となる設計思想です。この章では、ページを再利用可能な小さなコンポーネントに分割し、Props とコールバック関数を使って親子間の通信を実現する方法を学びます。


なぜコンポーネントを分割するのか?

App.jsx がどんどん長くなり、数百行のコードが密集して、保守や再利用が難しくなります。

コンポーネント分割の原則:各コンポーネントは1つのことだけを担当し、組み合わせて複雑な機能を実現する。

どの粒度が適切か?

ページ領域コンポーネントに分割すべきか理由
ナビゲーションバーであるすべてのページで必要、独立した機能
記事カードであるリスト内の各記事が同じ構造
カテゴリ絞り込みボタングループである独自の状態とインタラクションを持つ
単純なラッパーコンテナ否レイアウトのためだけであり、独立した動作がない

親から子へ:Props

React では、Props は関数のパラメータです——親コンポーネントが子コンポーネントを呼び出すときに、データを属性を通じて渡します。

例

// ファイルパス:src/components/BlogCard.jsx
// Props は関数の最初のパラメータであり、通常は分割代入して使用します
function BlogCard({ title, summary, date, category }) {
  return (
    <div className="card">
      <span className="tag">{category}</span>
      <h3>{title}</h3>
      <p>{summary}</p>
      <span className="date">{date}</span>
    </div>
  )
}

export default BlogCard

親コンポーネントは HTML 属性を書くようにデータを渡します:

例

// ファイルパス:src/App.jsx
import BlogCard from './components/BlogCard'

function App() {
  const articles = [
    { id: 1, title: 'React 入門', summary: 'ゼロから学ぶ React', date: '2024-05-10', category: 'React' },
    { id: 2, title: 'Promise 詳解', summary: '非同期をマスターする', date: '2024-05-08', category: 'JavaScript' },
  ]

  return (
    <div className="article-grid">
      {articles.map(article => (
        <BlogCard
          key={article.id}
          title={article.title}
          summary={article.summary}
          date={article.date}
          category={article.category}
        />
      ))}
    </div>
  )
}

Props は単方向データフロー:データは親コンポーネントから子コンポーネントへ流れます。子コンポーネントは受け取った Props を変更できません。変更が必要な場合は、親コンポーネントがコールバック関数を通じて処理する必要があります。


子から親へ:コールバック関数

子コンポーネントが親コンポーネントに「何が起こったか」を通知するには、親コンポーネントがコールバック関数を Props として渡す必要があります。

例

// ファイルパス:src/components/CategoryFilter.jsx
function CategoryFilter({ categories, activeCategory, onCategoryChange }) {
  return (
    <div className="filter-bar">
      {categories.map(cat => (
        <button
          key={cat}
          className={activeCategory === cat ? 'active' : ''}
          // クリック時に親コンポーネントから渡されたコールバック関数を呼び出す
          onClick={() => onCategoryChange(cat)}
        >
          {cat}
        </button>
      ))}
    </div>
  )
}

export default CategoryFilter

親コンポーネントがコールバック関数を渡す:

例

import { useState } from 'react'
import CategoryFilter from './components/CategoryFilter'

function App() {
  const [activeCategory, setActiveCategory] = useState('すべて')
  const categories = ['すべて', 'React', 'JavaScript', 'CSS']

  // コールバック関数:子コンポーネントのクリックイベントに応答する
  function handleCategoryChange(cat) {
    setActiveCategory(cat)
  }

  return (
    <CategoryFilter
      categories={categories}
      activeCategory={activeCategory}
      onCategoryChange={handleCategoryChange}
    />
  )
}

React と Vue3 の通信比較

シナリオVue3React
親から子へProps(defineProps)Props(関数パラメータ)
子から親へemit イベントProps コールバック関数
データの向き単方向単方向

Vue3 は emit メカニズムを使い、React はコールバック関数を使います——本質は同じで、構文が異なるだけです。


Props の分割とデフォルト値

例

// デフォルト値付きの Props
function BlogCard({
  title = '無題の記事',        // ES6 分割代入のデフォルト値
  summary = '要約なし',
  date = '',
  category = '未分類'
}) {
  return (
    <div className="card">
      <span className="tag">{category}</span>
      <h3>{title}</h3>
      <p>{summary}</p>
      {date && <span className="date">{date}</span>}
    </div>
  )
}

実践:3つのコンポーネントに分割する

ブログプロジェクトのコンポーネント構造は以下の通りです:

src/
├── components/
│   ├── NavBar.jsx       # 顶部导航栏
│   ├── BlogCard.jsx     # 单篇文章卡片
│   └── CategoryFilter.jsx  # 分类筛选按钮组
└── App.jsx

NavBar.jsx

例

// ファイルパス:src/components/NavBar.jsx
import '../App.css'

function NavBar() {
  return (
    <header className="navbar">
      <a href="/" className="logo">サンプル Blog</a>
      <nav>
        <a href="/">ホーム</a>
        <a href="#">について</a>
      </nav>
    </header>
  )
}

export default NavBar

App.jsx — 全コンポーネントの統合

例

// ファイルパス:src/App.jsx
import { useState, useMemo } from 'react'
import NavBar from './components/NavBar'
import BlogCard from './components/BlogCard'
import CategoryFilter from './components/CategoryFilter'
import './App.css'

function App() {
  const [articles] = useState([
    { id: 1, title: 'React 入門完全ガイド', summary: 'ゼロから始める React Hooks', date: '2024-05-10', category: 'React' },
    { id: 2, title: 'JS 非同期プログラミング詳解', summary: 'Promise と async/await をマスターする', date: '2024-05-08', category: 'JavaScript' },
    { id: 3, title: 'CSS Grid レイアウト実践', summary: 'Grid でレスポンシブレイアウトを実装する', date: '2024-05-05', category: 'CSS' },
    { id: 4, title: 'React Hooks 深掘り', summary: 'useState と useEffect を深く理解する', date: '2024-05-03', category: 'React' },
    { id: 5, title: 'Flexbox 完全ガイド', summary: '1記事でマスターするフレックスレイアウト', date: '2024-05-01', category: 'CSS' },
  ])

  const [activeCategory, setActiveCategory] = useState('すべて')

  const categories = useMemo(() => {
    const cats = articles.map(a => a.category)
    return ['すべて', ...new Set(cats)]
  }, [articles])

  const filteredArticles = useMemo(() => {
    if (activeCategory === 'すべて') return articles
    return articles.filter(a => a.category === activeCategory)
  }, [articles, activeCategory])

  return (
    <div className="app">
      <NavBar />

      <main className="container">
        <h2 className="section-title">最新記事</h2>

        {/* 子コンポーネント:props(categories, activeCategory)とコールバック(onCategoryChange)を受け取る */}
        <CategoryFilter
          categories={categories}
          activeCategory={activeCategory}
          onCategoryChange={setActiveCategory}
        />

        <p className="result-info">共 {filteredArticles.length} 篇</p>

        {filteredArticles.length === 0 ? (
          <p className="empty-tip">このカテゴリにはまだ記事がありません</p>
        ) : (
          <div className="article-grid">
            {filteredArticles.map(article => (
              <BlogCard
                key={article.id}
                title={article.title}
                summary={article.summary}
                date={article.date}
                category={article.category}
              />
            ))}
          </div>
        )}
      </main>

      <footer className="footer">
        <p>© 2024 サンプル Blog. Powered by React.</p>
      </footer>
    </div>
  )
}

export default App

本章のまとめ

この章では、React のコンポーネント通信の核心を習得しました:親から子へは Props(関数パラメータ)、子から親へは Props コールバック関数です。

ブログプロジェクトの3つのコンポーネント NavBar、BlogCard、CategoryFilter が配置され、コード構造がかなり明確になりました。

その他の拡張