コンポーネントの構成と再利用
コンポーネントの組み合わせは、レゴで遊ぶようなものです。すべてのピースがユニークである必要はありません。優れたコンポーネント設計とは、基本的な構成要素(サブコンポーネント)が柔軟であり、さまざまな方法で組み合わせて、どんな形でも作り出せることを意味します。
1. 学習内容
- 構成と継承
- キッズ用スロットおよびマルチスロットモード
- コンポーネント分解の原則(単一責任の原則)
- Render Props パターン
- 高階コンポーネント(HOC)入門
2. デザインシステムの物語
(1) 課題:ページごとに同じ作業を繰り返している
チャーリーは同社のフロントエンド・アーキテクトだ。彼は、どのページも「車輪の再発明」をしていることに気づいた:
// Page A:Custom Pop-up Window
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<div style={{ backgroundColor: 'white', padding: '24px', borderRadius: '8px' }}>
<h2>Confirm Deletion</h2>
<p>Are you sure you want to delete this record??</p>
<button>Cancel</button>
<button>Confirm</button>
</div>
</div>
// Page B:Another pop-up,But the style is slightly different
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<div style={{ backgroundColor: 'white', padding: '32px', borderRadius: '12px' }}>
<h2>Edit User</h2>
<input placeholder="Username" />
<input placeholder="Email" />
<button>Save</button>
<button>Cancel</button>
</div>
</div>
問題点:マスクレイヤーとコンテナのスタイルが2つのページで重複しています。ポップアップの角丸のサイズを変更したい場合、20ページを編集する必要があります。
(2) 組み合わせモデルの解法
// General Popup Component(Responsible only for structure and style,Not interested in the content)
function Modal({ isOpen, onClose, title, children }) {
if (!isOpen) return null
return (
<div style={overlayStyle} onClick={onClose}>
<div style={modalStyle} onClick={e => e.stopPropagation()}>
{title && <h2 style={{ margin: '0 0 16px 0' }}>{title}</h2>}
{children} {/* The content is determined by the user. */}
</div>
</div>
)
}
// Page A:Confirm Deletion
<Modal isOpen={showDelete} onClose={() => setShowDelete(false)} title="Confirm Deletion">
<p>Are you sure you want to delete this record??This action cannot be undone.。</p>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button onClick={() => setShowDelete(false)}>Cancel</button>
<button style={{ backgroundColor: '#ff4d4f', color: 'white' }}>Confirm Deletion</button>
</div>
</Modal>
// Page B:Edit User
<Modal isOpen={showEdit} onClose={() => setShowEdit(false)} title="Edit User">
<input placeholder="Username" />
<input placeholder="Email" />
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '16px' }}>
<button>Save</button>
<button onClick={() => setShowEdit(false)}>Cancel</button>
</div>
</Modal>
メリット:ポップアップのスタイルを一元管理できる(1か所を変更すればすべてに反映される);コンテンツは完全にユーザーが決定できる;1つのモーダルコンポーネントが20ページにわたって再利用されている。
graph TB
subgraph "Combination Mode"
A[Modal Components] --> B[Overlay]
A --> C[Container modal]
C --> D[Title title]
C --> E[children slot]
E --> F[The content is determined by the parent component]
end
style E fill:#1890ff,color:#fff
3. 子要素スロット
| スロットモード | 構文 | 使用例 |
|---|---|---|
| シングルスロット | {children} |
シンプルなコンテナ(カード、レイアウト) |
| マルチスロット | {props.header} {props.footer} |
マルチリージョンレイアウト(ページ、ダイアログ) |
| renderProps | {props.renderItem(item)} |
リスト/テーブルのカスタムレンダリング |
| 機能サブコンポーネント | {props.children(data)} |
データ伝播 + カスタムレンダリング |
(1) 基本的な使い方
children は、コンポーネントのタグで囲まれた内容を表す特別なプロップです。
// Component Definition
function Card({ title, children }) {
return (
<div style={{
border: '1px solid #f0f0f0',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px'
}}>
{title && <h3 style={{ margin: '0 0 12px 0', color: '#333' }}>{title}</h3>}
<div>{children}</div>
</div>
)
}
// How to Use 1:A single element
<Card title="Announcement">
<p>The system will go live tonight 22:00 Perform maintenance</p>
</Card>
// How to Use 2:Multiple elements
<Card title="User Information">
<p>Name:Alice</p>
<p>Characters:Administrator</p>
<p>Email:alice@example.com</p>
</Card>
// How to Use 3:and even other components
<Card title="Statistical Overview">
<StatCard label="Number of users" value={1234} />
<StatCard label="Number of Orders" value={567} />
</Card>
(2) マルチスロットモード
コンポーネントに複数のスロット位置が必要な場合は、名前付きプロップ(header、footer、main など)を使用できます:
function PageLayout({ header, sidebar, children, footer }) {
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header Socket */}
<header style={{ backgroundColor: '#001529', color: 'white', padding: '16px' }}>
{header}
</header>
<div style={{ display: 'flex', flex: 1 }}>
{/* Sidebar Slot */}
<aside style={{ width: '240px', backgroundColor: '#f5f5f5', padding: '16px' }}>
{sidebar}
</aside>
{/* Main Content Slot(children) */}
<main style={{ flex: 1, padding: '24px' }}>
{children}
</main>
</div>
{/* Bottom Slot */}
<footer style={{ backgroundColor: '#f0f0f0', padding: '12px', textAlign: 'center' }}>
{footer}
</footer>
</div>
)
}
// Usage
<PageLayout
header={<><h1>Management System</h1><nav>Navigation Menu</nav></>}
sidebar={<><h3>List of Features</h3><ul><li>User Management</li><li>Order Management</li></ul></>}
footer={<p>© 2026 web-tutorial.com</p>}
>
<h2>Welcome back,Administrator</h2>
<p>Today's Orders:128 orders</p>
<p>New Users:23 users</p>
</PageLayout>
(1) ▶ サンプル:汎用リストコンポーネント
// ============================================
// Example:Generic List Component(Multi-slot + children)
// ============================================
function DataList({ data, renderItem, header, emptyMessage = 'No data available' }) {
return (
<div style={{ border: '1px solid #f0f0f0', borderRadius: '8px' }}>
{/* Header Socket */}
{header && <div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa' }}>{header}</div>}
{/* List Contents */}
{data.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: '#999' }}>{emptyMessage}</div>
) : (
data.map((item, index) => (
<div key={item.id} style={{
padding: '12px 16px',
borderBottom: index < data.length - 1 ? '1px solid #f0f0f0' : 'none',
transition: 'background-color 0.2s'
}}
onMouseEnter={e => e.currentTarget.style.backgroundColor = '#fafafa'}
onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
>
{renderItem(item)}
</div>
))
)}
{/* children You can add additional action buttons */}
{data.length > 0 && <div style={{ padding: '12px 16px', borderTop: '1px solid #f0f0f0' }}>{data.length} records</div>}
</div>
)
}
// Examples of Use 1:User List
<DataList
data={users}
header={<h3 style={{ margin: 0 }}>User List</h3>}
renderItem={user => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{user.name}</span>
<span style={{ color: '#999' }}>{user.role}</span>
</div>
)}
/>
// Examples of Use 2:Product List
<DataList
data={products}
header={<h3 style={{ margin: 0 }}>Product List <button>Add Item</button></h3>}
renderItem={product => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{product.name}</span>
<span style={{ color: '#ff4d4f' }}>${product.price}</span>
</div>
)}
emptyMessage="No products have been listed yet."
/>
4. コンポーネント分解の原則
優れたコンポーネントは、優れた関数のようなものです。つまり、単一の責任を持ち、再利用可能で、テストしやすいものです。
| 原理 | 説明 | 反例 |
|---|---|---|
| 単一責任 | コンポーネントは1つのことだけを行う | コンポーネントはユーザー情報の表示とデータエクスポートの処理の両方を行う |
| インターフェースを最小限に抑える | プロパティは少ないほど良い(5以下) | コンポーネントには15個のプロパティがある |
| データは下方向に流れる | データは親から子へと一方向の流れとなる | 子コンポーネントはグローバル状態を直接変更する |
| 副作用なし | 同じプロパティを設定すれば同じ結果が得られる | コンポーネント内で直接DOMを操作したり、APIを呼び出したりできる |
(1) ▶ サンプル:分割前と分割後
// ============================================
// Example:Component Breakdown——from "Giant Components"to "Widget"
// ============================================
// ---- Before the split:One component does it all ----
function GiantUserPage({ userId }) {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [editing, setEditing] = React.useState(false)
const [name, setName] = React.useState('')
const [email, setEmail] = React.useState('')
const [posts, setPosts] = React.useState([])
// ... 100 Multiple lines of logic mixed together
return (
<div>
{/* User Information、Edit Form、The list of articles is all crammed together */}
</div>
)
}
// ---- After the split:Each to their own role ----
function UserPage({ userId }) {
return (
<div>
<UserProfile userId={userId} /> {/* Responsible only for displaying user information */}
<UserEditForm userId={userId} /> {/* Responsible only for editing user content */}
<UserPosts userId={userId} /> {/* Responsible only for displaying the list of articles */}
</div>
)
}
// Each subcomponent can be developed independently.、Test、Reuse
function UserProfile({ userId }) { /* ... */ }
function UserEditForm({ userId }) { /* ... */ }
function UserPosts({ userId }) { /* ... */ }
(1) 分割に関する経験則
If a component exceeds 150 lines
A function that exceeds 30 lines
One render Return more than 10 JSX Element
Props More than 5
→ Consider a split!
5. Render Props パターン
Render Props はもう 1 つのコンポジションパターンであり、親コンポーネントが関数プロパティを通じて子コンポーネントのレンダリング内容を制御できるようにするものです。
// ============================================
// Example:Render Props——Mouse Position Tracker
// ============================================
// Child component:Responsible only for"Track Mouse Position",Don't care"How to display"
function MouseTracker({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
function handleMouseMove(e) {
setPosition({ x: e.clientX, y: e.clientY })
}
return (
<div style={{ height: '300px', border: '1px solid #ddd' }} onMouseMove={handleMouseMove}>
{/* Let the parent component determine how to render the mouse position */}
{render(position)}
</div>
)
}
// Parent Component:Decision"How to display"
function App() {
return (
<div>
<h2>Render Props Example</h2>
{/* Usage 1:Display Coordinates */}
<MouseTracker render={({ x, y }) => (
<div style={{ padding: '20px' }}>
<p>Mouse Position:({x}, {y})</p>
</div>
)} />
{/* Usage 2:Display Coordinate Indicator */}
<MouseTracker render={({ x, y }) => (
<div style={{
position: 'absolute', left: x, top: y,
width: '20px', height: '20px',
backgroundColor: '#1890ff', borderRadius: '50%',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none'
}} />
)} />
</div>
)
}
注:React Hooksの導入以来、Render Propsのほとんどのユースケースは、より簡潔なカスタムフックに置き換えることができます。Render Propsは、主にレガシーコードで見られます。
6. 構成と継承
Reactチームは次のように明確に述べています。「継承は使わないでください。コンポジションを使ってください。」
// ❌ Inheritance Patterns(Don't write it that way.)
class BaseButton extends React.Component {
// ...Overriding Methods After Inheritance in a Subclass,React Not recommended
}
class PrimaryButton extends BaseButton { }
// ✅ Combination Mode(The Correct Way to Do It)
function Button({ variant = 'default', children, ...props }) {
const styles = {
primary: { backgroundColor: '#1890ff', color: 'white' },
danger: { backgroundColor: '#ff4d4f', color: 'white' },
default: { backgroundColor: '#f0f0f0', color: '#333' }
}
return <button style={styles[variant]} {...props}>{children}</button>
}
// Through Props Control Variants,rather than through inheritance
<Button variant="primary">Main Buttons</Button>
<Button variant="danger">Danger Button</Button>
<Button variant="default">Default Button</Button>
| 次元 | 組み合わせ(推奨) | 継承(非推奨) |
|---|---|---|
| 柔軟性 | 高—プロパティや子要素を自由に組み合わせ可能 | 低—固定された継承チェーン |
| テスト容易性 | 高—各コンポーネントを個別にテストできる | 低—親クラスの実装に依存する |
| 型安全性 | 良好—プロップの型が明示的に指定されている | 不良—メソッドのオーバーライドはエラーが発生しやすい |
7. 完全な例:再利用可能なダッシュボードカードシステム
// ============================================
// Complete Example:Dashboard Card System
// Features:Combination Mode + children + Props Integrated Use of
// ============================================
// ---- 1. Basic Components:Card Holder ----
function DashboardCard({ title, icon, action, children, width = '300px' }) {
return (
<div style={{
border: '1px solid #f0f0f0',
borderRadius: '8px',
width,
backgroundColor: 'white',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
}}>
{/* Card Header */}
<div style={{
display: 'flex', justifyContent: 'space-between',
alignItems: 'center', padding: '16px',
borderBottom: '1px solid #f0f0f0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{icon && <span>{icon}</span>}
<h3 style={{ margin: 0, fontSize: '16px' }}>{title}</h3>
</div>
{action && <div>{action}</div>}
</div>
{/* Card Content Slot */}
<div style={{ padding: '16px' }}>
{children}
</div>
</div>
)
}
// ---- 2. Statistics Card ----
function StatCard({ label, value, trend, color = '#1890ff' }) {
return (
<div style={{ textAlign: 'center', padding: '8px 0' }}>
<p style={{ color: '#666', fontSize: '14px', margin: '0 0 8px 0' }}>{label}</p>
<p style={{ fontSize: '32px', fontWeight: 'bold', color, margin: '0 0 8px 0' }}>{value}</p>
{trend !== undefined && (
<p style={{
color: trend >= 0 ? '#52c41a' : '#ff4d4f',
fontSize: '14px', margin: 0
}}>
{trend >= 0 ? '↑' : '↓'} {Math.abs(trend)}%
</p>
)}
</div>
)
}
// ---- 3. List Cards ----
function ListCard({ title, icon, items, renderItem, action, emptyText = 'No data available' }) {
return (
<DashboardCard title={title} icon={icon} action={action}>
{items.length === 0 ? (
<p style={{ textAlign: 'center', color: '#999', padding: '20px' }}>{emptyText}</p>
) : (
items.map((item, index) => (
<div key={item.id} style={{
padding: '8px 0',
borderBottom: index < items.length - 1 ? '1px solid #f5f5f5' : 'none'
}}>
{renderItem(item)}
</div>
))
)}
</DashboardCard>
)
}
// ---- 4. Usage:Complete Dashboard ----
function Dashboard() {
const stats = [
{ id: 'users', label: 'Total Users', value: '12,846', trend: 12 },
{ id: 'orders', label: 'Today's Orders', value: '328', trend: -3 },
{ id: 'revenue', label: 'Monthly Income', value: '$89,200', trend: 25 },
{ id: 'visits', label: 'Visits Today', value: '4,621', trend: 8 }
]
const recentOrders = [
{ id: 1, customer: 'Alice', product: 'React Tutorial', amount: 89, status: 'Completed' },
{ id: 2, customer: 'Bob', product: 'TypeScript Getting Started', amount: 59, status: 'Processing...' },
{ id: 3, customer: 'Charlie', product: 'Node.js Real-World Experience', amount: 129, status: 'Completed' }
]
return (
<div>
<h2>📊 Management Dashboard</h2>
{/* Statistical Card Rows(Grid Layout) */}
<div style={{ display: 'flex', gap: '16px', marginBottom: '24px', flexWrap: 'wrap' }}>
<DashboardCard title="Data Overview" icon="📈" width="100%">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
{stats.map(s => (
<StatCard key={s.id} {...s} />
))}
</div>
</DashboardCard>
</div>
{/* List of Recent Orders */}
<div style={{ display: 'flex', gap: '16px' }}>
<ListCard
title="Recent Orders"
icon="📋"
items={recentOrders}
action={<button style={btnStyle}>View All</button>}
renderItem={order => (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<p style={{ margin: 0, fontWeight: 'bold' }}>{order.customer}</p>
<p style={{ margin: '4px 0 0 0', color: '#666', fontSize: '13px' }}>{order.product}</p>
</div>
<div style={{ textAlign: 'right' }}>
<p style={{ margin: 0, color: '#ff4d4f' }}>${order.amount}</p>
<span style={{
fontSize: '12px', padding: '2px 6px', borderRadius: '4px',
backgroundColor: order.status === 'Completed' ? '#f6ffed' : '#fff7e6',
color: order.status === 'Completed' ? '#52c41a' : '#faad14'
}}>
{order.status}
</span>
</div>
</div>
)}
/>
{/* Custom Cards:Use directly children */}
<DashboardCard title="Quick Actions" icon="⚡" width="300px">
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button style={actionBtnStyle}>📝 Create a New Order</button>
<button style={actionBtnStyle}>👤 Add User</button>
<button style={actionBtnStyle}>📊 Generate Reports</button>
<button style={actionBtnStyle}>⚙️ System Settings</button>
</div>
</DashboardCard>
</div>
</div>
)
}
const btnStyle = {
padding: '4px 12px', border: '1px solid #d9d9d9',
borderRadius: '4px', backgroundColor: 'white',
cursor: 'pointer', fontSize: '12px'
}
const actionBtnStyle = {
width: '100%', padding: '10px', border: 'none',
borderRadius: '4px', backgroundColor: '#f5f5f5',
cursor: 'pointer', textAlign: 'left', fontSize: '14px',
transition: 'background-color 0.2s'
}
// Hover effects are enabled via onMouseEnter/Leave Add,Omitted here
期待される出力:上部にデータ概要カード(4つの指標を含む)、左下に最近の注文一覧、右下にクイックアクションボタンのグループが配置された、完成したダッシュボードページ。すべてのカードはコンポジションパターンを使用して構築されています。
(1) ▶ サンプル:Render Props — ソート可能なテーブル
function SortableTable({ data, columns, renderCell }) {
const [sortKey, setSortKey] = useState(null)
const [sortDir, setSortDir] = useState('asc')
const sorted = useMemo(() => {
if (!sortKey) return data
return [...data].sort((a, b) => {
const diff = a[sortKey] > b[sortKey] ? 1 : a[sortKey] < b[sortKey] ? -1 : 0
return sortDir === 'asc' ? diff : -diff
})
}, [data, sortKey, sortDir])
function handleSort(key) {
if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc')
else { setSortKey(key); setSortDir('asc') }
}
return (
<table border={1} cellPadding={8} style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
{columns.map(col => (
<th key={col.key} onClick={() => handleSort(col.key)} style={{ cursor: 'pointer' }}>
{col.label} {sortKey === col.key ? (sortDir === 'asc' ? '↑' : '↓') : ''}
</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((row, i) => (
<tr key={i}>{columns.map(col => <td key={col.key}>{renderCell(row, col.key)}</td>)}</tr>
))}
</tbody>
</table>
)
}
function App() {
const users = [
{ name: 'Alice', score: 92, role: 'Admin' },
{ name: 'Bob', score: 85, role: 'Editor' },
{ name: 'Charlie', score: 78, role: 'Viewer' },
]
const cols = [{ key: 'name', label: 'Name' }, { key: 'score', label: 'Score' }, { key: 'role', label: 'Role' }]
return (
<div style={{ maxWidth: 500, margin: '0 auto' }}>
<h3>Sortable Users</h3>
<SortableTable data={users} columns={cols}
renderCell={(row, key) => key === 'score' ? `${row[key]} pts` : row[key]} />
</div>
)
}
(2) ▶ サンプル:レイアウトの組み合わせ—複数領域レイアウト
function AppLayout({ sidebar, header, content, footer }) {
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
{sidebar && <aside style={{ width: 200, background: '#f5f5f5', padding: 16 }}>{sidebar}</aside>}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{header && <header style={{ padding: '12px 16px', background: '#1890ff', color: 'white' }}>{header}</header>}
<main style={{ flex: 1, padding: 16 }}>{content}</main>
{footer && <footer style={{ padding: '8px 16px', background: '#fafafa', fontSize: 12, color: '#999' }}>{footer}</footer>}
</div>
</div>
)
}
function Dashboard() {
return (
<AppLayout
sidebar={<nav><p>Menu</p><ul><li>Home</li><li>Settings</li></ul></nav>}
header={<h2 style={{ margin: 0 }}>Dashboard</h2>}
content={<p>Welcome back, Alice! You have 5 new tasks.</p>}
footer={<span>© 2026 SaaS App</span>}
/>
)
}
(3) ▶ サンプル:コンポジションと継承の比較 — ポップアップコンポーネント
function Modal({ isOpen, onClose, title, children, footer }) {
if (!isOpen) return null
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}
onClick={onClose}>
<div style={{ background: 'white', borderRadius: 8, padding: 24, minWidth: 320, maxWidth: 500 }}
onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<h3 style={{ margin: 0 }}>{title}</h3>
<button onClick={onClose} style={{ border: 'none', background: 'none', cursor: 'pointer', fontSize: 18 }}>✕</button>
</div>
<div style={{ marginBottom: 16 }}>{children}</div>
{footer && <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>{footer}</div>}
</div>
</div>
)
}
function ConfirmModal({ isOpen, onConfirm, onCancel, message }) {
return (
<Modal isOpen={isOpen} onClose={onCancel} title="Confirm"
footer={<>
<button onClick={onCancel} style={{ padding: '6px 16px', cursor: 'pointer' }}>Cancel</button>
<button onClick={onConfirm} style={{ padding: '6px 16px', background: '#ff4d4f', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Confirm</button>
</>}>
<p>{message}</p>
</Modal>
)
}
function FormModal({ isOpen, onClose }) {
const [value, setValue] = useState('')
return (
<Modal isOpen={isOpen} onClose={onClose} title="New Item"
footer={<button onClick={() => { alert(value); onClose() }} style={{ padding: '6px 16px', background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Submit</button>}>
<input value={value} onChange={e => setValue(e.target.value)} placeholder="Enter name" style={{ width: '100%', padding: 8, borderRadius: 4 }} />
</Modal>
)
}
❓ よくある質問
childrenとPropsは、それぞれどのような場合に使用すべきですか?childrenは、「可変コンテンツ領域」を持つコンテナコンポーネント(モーダル、カード、レイアウトなど)に使用します。Propsは、「設定可能なコンテンツ」(ボタンの色やタイトルテキストなど)を持つコンポーネントに使用します。簡単に言えば、構造要素にはchildrenを、設定要素にはPropsを使用します。UserForm コンポーネントに 12 個のプロップがある場合は、これを 3 つの子コンポーネント、UserBasicInfo(名前、メールアドレス、電話番号)、UserAddress(省、市、区、詳細住所)、UserSettings(役割、権限)に分割します。useXxx()1行のコードでロジックを再利用でき、よりフラットで読みやすいコードになります。Render Propsが有利なのは、再利用されるロジックが「UIの一部をレンダリングする」必要がある場合(たとえば、ツールチップコンポーネントが子要素の位置を知る必要がある場合など)に限られます。📖 まとめ
- コンポジションは継承よりも優れている—Reactでは、100%コンポジションを使用し、継承は一切使用しません。
childrenは、「スロット」パターンを実装した特別なプロップであり、コンポーネントを柔軟にネストできるようにします- マルチスロットモードを、名前付きプロパティ(header/footer/sidebar)と組み合わせて使用することで、複雑なレイアウトを実装できます
- コンポーネントの分解は、単一責任の原則に従います。つまり、1つのコンポーネントは1つのことだけを行います。
- Render Props はレンダリング関数を渡しますが、Hooks の方が優れた代替手段です。
📝 練習問題
- 基本問題(難易度 ⭐):
count(数値)とchildren(コンテンツ)を受け取り、右上隅に赤い上付き数字を表示するBadgeコンポーネントを作成してください。 - 上級演習(難易度 ⭐⭐):
Tabsコンポーネントを作成し、コンポジットパターンを用いてラベルの切り替えを実装してください:<Tabs><TabPanel label="Tab1">inside content 1</TabPanel><TabPanel label="Tab2">inside content 2</TabPanel></Tabs>。 - 課題(難易度:⭐⭐⭐):Render Props パターン
<Table data={users} columns={[{ title: 'Name', render: u => u.name }, { title: 'Age', render: u => u.age }]} />を使用してTableコンポーネントを作成し、ソート機能とゼブラハイライト機能をサポートしてください。



