コンポーネントの分割 — Props とイベントコールバック
コンポーネント化は React の最も中核となる設計思想です。この章では、ページを再利用可能な小さなコンポーネントに分割し、Props とコールバック関数を使って親子間の通信を実現する方法を学びます。
なぜコンポーネントを分割するのか?
App.jsx がどんどん長くなり、数百行のコードが密集して、保守や再利用が難しくなります。
コンポーネント分割の原則:各コンポーネントは1つのことだけを担当し、組み合わせて複雑な機能を実現する。
どの粒度が適切か?
| ページ領域 | コンポーネントに分割すべきか | 理由 |
|---|---|---|
| ナビゲーションバー | である | すべてのページで必要、独立した機能 |
| 記事カード | である | リスト内の各記事が同じ構造 |
| カテゴリ絞り込みボタングループ | である | 独自の状態とインタラクションを持つ |
| 単純なラッパーコンテナ | 否 | レイアウトのためだけであり、独立した動作がない |
親から子へ:Props
React では、Props は関数のパラメータです——親コンポーネントが子コンポーネントを呼び出すときに、データを属性を通じて渡します。
例
// 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 属性を書くようにデータを渡します:
例
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 として渡す必要があります。
例
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 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 の通信比較
| シナリオ | Vue3 | React |
|---|---|---|
| 親から子へ | Props(defineProps) | Props(関数パラメータ) |
| 子から親へ | emit イベント | Props コールバック関数 |
| データの向き | 単方向 | 単方向 |
Vue3 は emit メカニズムを使い、React はコールバック関数を使います——本質は同じで、構文が異なるだけです。
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
例
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 — 全コンポーネントの統合
例
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 が配置され、コード構造がかなり明確になりました。
その他の拡張