404 Not Found

404 Not Found


nginx

コンポーネントの構成と再利用

コンポーネントの組み合わせは、レゴで遊ぶようなものです。すべてのピースがユニークである必要はありません。優れたコンポーネント設計とは、基本的な構成要素(サブコンポーネント)が柔軟であり、さまざまな方法で組み合わせて、どんな形でも作り出せることを意味します。


1. 学習内容



2. デザインシステムの物語

(1) 課題:ページごとに同じ作業を繰り返している

チャーリーは同社のフロントエンド・アーキテクトだ。彼は、どのページも「車輪の再発明」をしていることに気づいた:

JSX
// 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) 組み合わせモデルの解法

JSX
// 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ページにわたって再利用されている。

100%
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 は、コンポーネントのタグで囲まれた内容を表す特別なプロップです。

JSX
// 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) マルチスロットモード

コンポーネントに複数のスロット位置が必要な場合は、名前付きプロップ(headerfootermain など)を使用できます:

JSX
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) ▶ サンプル:汎用リストコンポーネント

JSX
// ============================================
// 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) ▶ サンプル:分割前と分割後

JSX
// ============================================
// 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) 分割に関する経験則

BASH
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 つのコンポジションパターンであり、親コンポーネントが関数プロパティを通じて子コンポーネントのレンダリング内容を制御できるようにするものです。

JSX
// ============================================
// 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チームは次のように明確に述べています。「継承は使わないでください。コンポジションを使ってください。」

JSX
// ❌ 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. 完全な例:再利用可能なダッシュボードカードシステム

JSX
// ============================================
// 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 — ソート可能なテーブル

JSX
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) ▶ サンプル:レイアウトの組み合わせ—複数領域レイアウト

JSX
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) ▶ サンプル:コンポジションと継承の比較 — ポップアップコンポーネント

JSX
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>
  )
}
▶ 試してみよう

❓ よくある質問

Q childrenPropsは、それぞれどのような場合に使用すべきですか?
A childrenは、「可変コンテンツ領域」を持つコンテナコンポーネント(モーダル、カード、レイアウトなど)に使用します。Propsは、「設定可能なコンテンツ」(ボタンの色やタイトルテキストなど)を持つコンポーネントに使用します。簡単に言えば、構造要素にはchildrenを、設定要素にはPropsを使用します。
Q Render Propsとフックのどちらを選べばよいですか?
A 新規のコードでは、カスタムフックを優先してください。Render Propsは、状態ロジックとUI構造の両方が関わるシナリオ(プレースホルダーDOMを必要とするMouseTrackerなど)に適しています。フックは、状態ロジックのみが関わるシナリオ(座標のみを返し、UIをレンダリングしないuseMousePositionなど)に適しています。Reactは公式にHooksを推奨しています。
Q プロップが多すぎる場合はどうすればよいですか?
A これは、コンポーネントの担当範囲が広すぎることを示しており、分割する必要があります。推奨される対処法:各グループが子コンポーネントの担当範囲に対応するように、プロップをグループ分けしてください。たとえば、UserForm コンポーネントに 12 個のプロップがある場合は、これを 3 つの子コンポーネント、UserBasicInfo(名前、メールアドレス、電話番号)、UserAddress(省、市、区、詳細住所)、UserSettings(役割、権限)に分割します。
Q Render PropsとHooks、どちらが優れていますか?
A Hooksの方が優れています。Render Propsはバージョン16.7以前のReactで使用されていた再利用パターンでしたが、ネストされたコールバック関数を必要とし、「コールバック地獄」を引き起こしていました。Hooksを使えば、たったuseXxx()1行のコードでロジックを再利用でき、よりフラットで読みやすいコードになります。Render Propsが有利なのは、再利用されるロジックが「UIの一部をレンダリングする」必要がある場合(たとえば、ツールチップコンポーネントが子要素の位置を知る必要がある場合など)に限られます。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐)count(数値)とchildren(コンテンツ)を受け取り、右上隅に赤い上付き数字を表示する Badge コンポーネントを作成してください。
  2. 上級演習(難易度 ⭐⭐)Tabs コンポーネントを作成し、コンポジットパターンを用いてラベルの切り替えを実装してください:<Tabs><TabPanel label="Tab1">inside content 1</TabPanel><TabPanel label="Tab2">inside content 2</TabPanel></Tabs>
  3. 課題(難易度:⭐⭐⭐):Render Props パターン <Table data={users} columns={[{ title: 'Name', render: u => u.name }, { title: 'Age', render: u => u.age }]} /> を使用して Table コンポーネントを作成し、ソート機能とゼブラハイライト機能をサポートしてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%