React: 组件组合与复用
最后更新:2026-08-26
组件组合就像 搭乐高——不需要每一块都是独一无二的。好的组件设计是:基础积木(小组件)灵活可拼装,组合在一起能搭出任何造型。
1. 你将学到
- 组合模式 vs 继承
- children 插槽和多插槽模式
- 组件拆分原则(单一职责)
- Render Props 模式
- 高阶组件(HOC)简介
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 个页面复用。
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(比如 header、footer、main):
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="还没有上架任何商品"
/>
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>
)
}
▶ 示例 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 组件需要知道子元素的位置)。📖 小节
- 组合优于继承——React 中 100% 用组合,不用继承
children是特殊的 Props,实现"插槽"模式,让组件灵活嵌套- 多插槽模式可以用具名 Props(header/footer/sidebar)实现复杂布局
- 组件拆分遵循单一职责原则:一个组件只做一件事
- Render Props 是传递渲染函数,Hooks 是更好的替代方案
📝 作业
- 基础题(难度⭐):创建一个
Badge组件,接收count(数字)、children(内容),在右上角显示红色角标数字。 - 进阶题(难度⭐⭐):创建一个
Tabs组件,使用组合模式实现标签切换:<Tabs><TabPanel label="Tab1">内容1</TabPanel><TabPanel label="Tab2">内容2</TabPanel></Tabs>。 - 挑战题(难度⭐⭐⭐):创建一个
Table组件,使用 Render Props 模式:<Table data={users} columns={[{ title: '姓名', render: u => u.name }, { title: '年龄', render: u => u.age }]} />,支持排序和斑马纹。