React: 组件组合与复用

最后更新:2026-08-26

组件组合就像 搭乐高——不需要每一块都是独一无二的。好的组件设计是:基础积木(小组件)灵活可拼装,组合在一起能搭出任何造型。


1. 你将学到



2. 一个设计系统的故事

(1) 痛点:每个页面重新造轮子

Charlie 是公司的前端架构师。他发现每个页面都在重复造轮子:

JSX
// 页面 A:自定义弹窗
<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>确认删除</h2>
    <p>确定要删除这条记录吗?</p>
    <button>取消</button>
    <button>确认</button>
  </div>
</div>

// 页面 B:又是弹窗,但样式略有不同
<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>编辑用户</h2>
    <input placeholder="用户名" />
    <input placeholder="邮箱" />
    <button>保存</button>
    <button>取消</button>
  </div>
</div>
▶ 试一试

问题:遮罩层样式、容器样式在两个页面重复写了。如果要改弹窗的圆角大小,要改 20 个页面。

(2) 组合模式的解法

JSX
// 通用弹窗组件(只负责结构和样式,不关心内容)
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}  {/* 内容由使用方决定 */}
      </div>
    </div>
  )
}

// 页面 A:确认删除
<Modal isOpen={showDelete} onClose={() => setShowDelete(false)} title="确认删除">
  <p>确定要删除这条记录吗?此操作不可撤销。</p>
  <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
    <button onClick={() => setShowDelete(false)}>取消</button>
    <button style={{ backgroundColor: '#ff4d4f', color: 'white' }}>确认删除</button>
  </div>
</Modal>

// 页面 B:编辑用户
<Modal isOpen={showEdit} onClose={() => setShowEdit(false)} title="编辑用户">
  <input placeholder="用户名" />
  <input placeholder="邮箱" />
  <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '16px' }}>
    <button>保存</button>
    <button onClick={() => setShowEdit(false)}>取消</button>
  </div>
</Modal>
▶ 试一试

收益:弹窗样式统一管理(改一处全改),内容完全由使用方决定,单个 Modal 组件被 20 个页面复用。

100%
graph TB
    subgraph "组合模式"
        A[Modal 组件] --> B[遮罩层 overlay]
        A --> C[容器 modal]
        C --> D[标题 title]
        C --> E[**children 插槽**]
        E --> F[内容由父组件决定]
    end
    
    style E fill:#1890ff,color:#fff


3. children 插槽

插槽模式 语法 适用场景
单插槽 {children} 简单包裹容器(Card、Layout)
多插槽 {props.header} {props.footer} 多区域布局(Page、Dialog)
renderProps {props.renderItem(item)} 列表/表格的自定义渲染
函数子组件 {props.children(data)} 数据下发 + 自定义渲染

(1) 基础用法

children 是一个特殊的 Prop,代表组件标签之间的内容

JSX
// 组件定义
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>
  )
}

// 使用方式 1:单个元素
<Card title="公告">
  <p>系统将于今晚 22:00 进行维护</p>
</Card>

// 使用方式 2:多个元素
<Card title="用户信息">
  <p>姓名:Alice</p>
  <p>角色:管理员</p>
  <p>邮箱:alice@example.com</p>
</Card>

// 使用方式 3:甚至其他组件
<Card title="统计概览">
  <StatCard label="用户数" value={1234} />
  <StatCard label="订单数" value={567} />
</Card>
▶ 试一试

(2) 多插槽模式

如果组件需要多个插槽位置,可以用具名 Props(比如 headerfootermain):

JSX
function PageLayout({ header, sidebar, children, footer }) {
  return (
    <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
      {/* 头部插槽 */}
      <header style={{ backgroundColor: '#001529', color: 'white', padding: '16px' }}>
        {header}
      </header>

      <div style={{ display: 'flex', flex: 1 }}>
        {/* 侧边栏插槽 */}
        <aside style={{ width: '240px', backgroundColor: '#f5f5f5', padding: '16px' }}>
          {sidebar}
        </aside>
        
        {/* 主内容插槽(children) */}
        <main style={{ flex: 1, padding: '24px' }}>
          {children}
        </main>
      </div>

      {/* 底部插槽 */}
      <footer style={{ backgroundColor: '#f0f0f0', padding: '12px', textAlign: 'center' }}>
        {footer}
      </footer>
    </div>
  )
}

// 使用
<PageLayout
  header={<><h1>管理系统</h1><nav>导航菜单</nav></>}
  sidebar={<><h3>功能列表</h3><ul><li>用户管理</li><li>订单管理</li></ul></>}
  footer={<p>© 2026 web-tutorial.com</p>}
>
  <h2>欢迎回来,管理员</h2>
  <p>今日订单:128 单</p>
  <p>新增用户:23 人</p>
</PageLayout>
▶ 试一试

▶ 示例:通用列表组件

JSX 📖 仅展示
// ============================================
// 示例:通用列表组件(多插槽 + children)
// ============================================

function DataList({ data, renderItem, header, emptyMessage = '暂无数据' }) {
  return (
    <div style={{ border: '1px solid #f0f0f0', borderRadius: '8px' }}>
      {/* 头部插槽 */}
      {header && <div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa' }}>{header}</div>}
      
      {/* 列表内容 */}
      {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 可以放额外的操作按钮 */}
      {data.length > 0 && <div style={{ padding: '12px 16px', borderTop: '1px solid #f0f0f0' }}>{data.length} 条记录</div>}
    </div>
  )
}

// 使用示例 1:用户列表
<DataList
  data={users}
  header={<h3 style={{ margin: 0 }}>用户列表</h3>}
  renderItem={user => (
    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
      <span>{user.name}</span>
      <span style={{ color: '#999' }}>{user.role}</span>
    </div>
  )}
/>

// 使用示例 2:商品列表
<DataList
  data={products}
  header={<h3 style={{ margin: 0 }}>商品列表 <button>添加商品</button></h3>}
  renderItem={product => (
    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
      <span>{product.name}</span>
      <span style={{ color: '#ff4d4f' }}>${product.price}</span>
    </div>
  )}
  emptyMessage="还没有上架任何商品"
/>
逻辑代码 48 行(超过 40 行限制,仅展示)

4. 组件拆分原则

好的组件像好的函数——职责单一、可复用、易测试。

原则 说明 反例
单一职责 一个组件只做一件事 一个组件既显示用户信息又处理数据导出
接口最小化 Props 越少越好(≤5 个) 一个组件有 15 个 Props
数据向下 数据从父到子单向流动 子组件直接修改全局状态
无副作用 相同 Props 渲染相同结果 组件内部直接修改 DOM 或调 API

▶ 示例:拆分前 vs 拆分后

JSX
// ============================================
// 示例:组件拆分——从"巨型组件"到"小组件"
// ============================================

// ---- 拆分前:一个组件做所有事 ----
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 多行逻辑混合在一起

  return (
    <div>
      {/* 用户信息、编辑表单、文章列表全部挤在一起 */}
    </div>
  )
}

// ---- 拆分后:各司其职 ----
function UserPage({ userId }) {
  return (
    <div>
      <UserProfile userId={userId} />  {/* 只负责展示用户信息 */}
      <UserEditForm userId={userId} />  {/* 只负责编辑用户 */}
      <UserPosts userId={userId} />     {/* 只负责显示文章列表 */}
    </div>
  )
}

// 每个子组件可以独立开发、测试、复用
function UserProfile({ userId }) { /* ... */ }
function UserEditForm({ userId }) { /* ... */ }
function UserPosts({ userId }) { /* ... */ }
▶ 试一试

(1) 拆分的经验法则

BASH
一个组件如果超过 150 行
一个函数超过 30 行    
一个 render 返回超过 10 个 JSX 元素
Props 超过 5 个

→ 考虑拆分!


5. Render Props 模式

Render Props 是另一种组合模式——通过一个函数 Prop 让父组件控制子组件的渲染内容。

JSX
// ============================================
// 示例:Render Props——鼠标位置跟踪器
// ============================================

// 子组件:只负责"跟踪鼠标位置",不关心"怎么显示"
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}>
      {/* 让父组件决定如何渲染鼠标位置 */}
      {render(position)}
    </div>
  )
}

// 父组件:决定"怎么显示"
function App() {
  return (
    <div>
      <h2>Render Props 示例</h2>

      {/* 用法 1:显示坐标 */}
      <MouseTracker render={({ x, y }) => (
        <div style={{ padding: '20px' }}>
          <p>鼠标位置:({x}, {y})</p>
        </div>
      )} />

      {/* 用法 2:显示坐标指示器 */}
      <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 的需求可以用自定义 Hook 替代(更简洁)。Render Props 在旧代码中常见。



6. 组合 vs 继承

React 官方明确说:不要用继承。用组合。

JSX
// ❌ 继承模式(不要这样写)
class BaseButton extends React.Component {
  // ...子类继承后覆盖方法,React 不推荐
}
class PrimaryButton extends BaseButton { }

// ✅ 组合模式(正确做法)
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>
}

// 通过 Props 控制变体,而不是通过继承
<Button variant="primary">主要按钮</Button>
<Button variant="danger">危险按钮</Button>
<Button variant="default">默认按钮</Button>
▶ 试一试
维度 组合(推荐) 继承(不推荐)
灵活性 高——通过 Props 和 children 自由组合 低——继承链固定
可测试性 高——每个组件可独立测试 低——依赖父类实现
类型安全 好——Props 类型明确 差——方法覆盖易出错


7. 完整示例:可复用的 Dashboard 卡片系统

JSX
// ============================================
// 完整示例:Dashboard 卡片系统
// 功能:组合模式 + children + Props 的综合运用
// ============================================

// ---- 1. 基础组件:卡片容器 ----
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)'
    }}>
      {/* 卡片头 */}
      <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>

      {/* 卡片内容插槽 */}
      <div style={{ padding: '16px' }}>
        {children}
      </div>
    </div>
  )
}

// ---- 2. 统计卡片 ----
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. 列表卡片 ----
function ListCard({ title, icon, items, renderItem, action, emptyText = '暂无数据' }) {
  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. 使用:完整 Dashboard ----
function Dashboard() {
  const stats = [
    { id: 'users', label: '总用户', value: '12,846', trend: 12 },
    { id: 'orders', label: '今日订单', value: '328', trend: -3 },
    { id: 'revenue', label: '月收入', value: '$89,200', trend: 25 },
    { id: 'visits', label: '今日访问', value: '4,621', trend: 8 }
  ]

  const recentOrders = [
    { id: 1, customer: 'Alice', product: 'React 教程', amount: 89, status: '已完成' },
    { id: 2, customer: 'Bob', product: 'TypeScript 入门', amount: 59, status: '处理中' },
    { id: 3, customer: 'Charlie', product: 'Node.js 实战', amount: 129, status: '已完成' }
  ]

  return (
    <div>
      <h2>📊 管理仪表盘</h2>

      {/* 统计卡片行(网格布局) */}
      <div style={{ display: 'flex', gap: '16px', marginBottom: '24px', flexWrap: 'wrap' }}>
        <DashboardCard title="数据概览" icon="📈" width="100%">
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
            {stats.map(s => (
              <StatCard key={s.id} {...s} />
            ))}
          </div>
        </DashboardCard>
      </div>

      {/* 最近订单列表 */}
      <div style={{ display: 'flex', gap: '16px' }}>
        <ListCard
          title="最近订单"
          icon="📋"
          items={recentOrders}
          action={<button style={btnStyle}>查看全部</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 === '已完成' ? '#f6ffed' : '#fff7e6',
                  color: order.status === '已完成' ? '#52c41a' : '#faad14'
                }}>
                  {order.status}
                </span>
              </div>
            </div>
          )}
        />

        {/* 自定义卡片:直接用 children */}
        <DashboardCard title="快捷操作" icon="⚡" width="300px">
          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
            <button style={actionBtnStyle}>📝 新建订单</button>
            <button style={actionBtnStyle}>👤 添加用户</button>
            <button style={actionBtnStyle}>📊 生成报表</button>
            <button style={actionBtnStyle}>⚙️ 系统设置</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'
}
// 悬停效果通过 onMouseEnter/Leave 添加,这里省略

预期输出:一个完整的 Dashboard 页面,顶部为数据概览卡片(4 个统计项),下方左侧为最近订单列表,右侧为快捷操作按钮组。所有卡片都通过组合模式构建。


▶ 示例 3: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>
  )
}
逻辑代码 48 行(超过 40 行限制,仅展示)

▶ 示例 4:Layout 组合——多区域布局

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>}
    />
  )
}
▶ 试一试

▶ 示例 5:组合 vs 继承——弹窗组件

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 什么时候用 children,什么时候用 Props?
A children 用于"内容区域不固定"的容器组件(弹窗、卡片、布局)。Props 用于"内容可参数化"的组件(按钮颜色、标题文字)。简单来说:结构性的用 children,配置性的用 Props。
Q Render Props 和 Hooks 怎么选?
A 新代码优先用自定义 Hook。Render Props 适合"既有状态逻辑又有 UI 结构"的场景(如 MouseTracker 需要占位 DOM)。Hooks 适合"只有状态逻辑"的场景(如 useMousePosition 只返回坐标,不渲染任何 UI)。React 官方推荐 Hooks。
Q Props 太多怎么办?
A 说明这个组件职责过重,需要拆分。建议:把 Props 分组,每组对应一个子组件的职责。比如一个 UserForm 组件如果有 12 个 Props → 拆成 UserBasicInfo(姓名、邮箱、电话)、UserAddress(省、市、区、详细地址)、UserSettings(角色、权限)三个子组件。
Q Render Props 和 Hooks 哪个更好?
A Hooks 是更好的选择。Render Props 是 React 16.7 之前的复用模式,需要嵌套回调函数,导致"嵌套地狱"(callback hell)。Hooks 用 useXxx() 一行代码就能复用逻辑,代码扁平、可读性强。只有当复用的逻辑需要"渲染一段 UI"时,Render Props 才有优势(如 Tooltip 组件需要知道子元素的位置)。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 Badge 组件,接收 count(数字)、children(内容),在右上角显示红色角标数字。
  2. 进阶题(难度⭐⭐):创建一个 Tabs 组件,使用组合模式实现标签切换:<Tabs><TabPanel label="Tab1">内容1</TabPanel><TabPanel label="Tab2">内容2</TabPanel></Tabs>
  3. 挑战题(难度⭐⭐⭐):创建一个 Table 组件,使用 Render Props 模式:<Table data={users} columns={[{ title: '姓名', render: u => u.name }, { title: '年龄', render: u => u.age }]} />,支持排序和斑马纹。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏