3.2.6 积木该多大才合适——组件设计
一句话破题
好的组件像乐高积木——足够小可以灵活组合,足够完整可以独立使用。
核心价值
组件设计决定了代码的可维护性。设计不当的组件要么过于庞大难以修改,要么过于碎片化难以理解。
单一职责原则
一个组件应该只有一个变更的理由。
tsx
// 错误:一个组件承担太多职责
function UserDashboard() {
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
const [notifications, setNotifications] = useState([])
// 获取用户数据
// 获取帖子数据
// 获取通知数据
// 处理用户操作
// 渲染所有内容...
return (/* 数百行 JSX */)
}
// 正确:拆分为多个专注的组件
function UserDashboard() {
return (
<div>
<UserProfile />
<UserPosts />
<NotificationPanel />
</div>
)
}展示组件 vs 容器组件
| 类型 | 职责 | 特点 |
|---|---|---|
| 展示组件 | 渲染 UI | 接收 Props,无状态逻辑 |
| 容器组件 | 管理数据 | 处理 state、effects、数据获取 |
tsx
// 展示组件:只负责渲染
interface UserCardProps {
name: string
avatar: string
email: string
}
function UserCard({ name, avatar, email }: UserCardProps) {
return (
<div className="card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
</div>
)
}
// 容器组件:负责数据
function UserCardContainer({ userId }: { userId: string }) {
const { data: user, loading } = useFetch(`/api/users/${userId}`)
if (loading) return <Skeleton />
return <UserCard {...user} />
}组合模式
通过 children 和插槽实现灵活组合:
tsx
// 基础 Card 组件
interface CardProps {
children: React.ReactNode
header?: React.ReactNode
footer?: React.ReactNode
}
function Card({ children, header, footer }: CardProps) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
)
}
// 灵活使用
<Card
header={<h2>用户信息</h2>}
footer={<button>编辑</button>}
>
<p>姓名:张三</p>
<p>邮箱:zhang@example.com</p>
</Card>复合组件模式
对于复杂 UI,将相关组件组合在一起:
tsx
// 定义复合组件
const Tabs = ({ children, defaultValue }: TabsProps) => {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
)
}
Tabs.List = function TabsList({ children }) {
return <div className="tabs-list">{children}</div>
}
Tabs.Tab = function Tab({ value, children }) {
const { activeTab, setActiveTab } = useTabsContext()
return (
<button
className={activeTab === value ? 'active' : ''}
onClick={() => setActiveTab(value)}
>
{children}
</button>
)
}
Tabs.Panel = function TabPanel({ value, children }) {
const { activeTab } = useTabsContext()
if (activeTab !== value) return null
return <div>{children}</div>
}
// 使用
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Tab value="tab1">标签1</Tabs.Tab>
<Tabs.Tab value="tab2">标签2</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="tab1">内容1</Tabs.Panel>
<Tabs.Panel value="tab2">内容2</Tabs.Panel>
</Tabs>组件拆分时机
需要拆分的信号:
- 组件超过 200 行代码
- 有明显独立的 UI 区块
- 逻辑可以被复用
- 需要独立测试某部分
- 多人协作时需要减少冲突
不需要拆分的情况:
- 拆分后只在一处使用
- 拆分导致 Props 传递更复杂
- 拆分后反而难以理解
文件组织
components/
├── ui/ # 通用 UI 组件
│ ├── Button.tsx
│ ├── Card.tsx
│ └── Input.tsx
├── features/ # 业务组件
│ ├── UserProfile/
│ │ ├── index.tsx
│ │ ├── UserAvatar.tsx
│ │ └── UserStats.tsx
│ └── PostList/
│ ├── index.tsx
│ ├── PostCard.tsx
│ └── PostActions.tsx
└── layouts/ # 布局组件
├── Header.tsx
└── Sidebar.tsxAI 协作指南
核心意图:让 AI 帮你分析和重构组件结构。
需求定义公式:
- 功能描述:这个组件目前 [做了什么]
- 交互方式:其中 [哪些部分] 是独立的
- 预期效果:希望拆分后 [达到什么效果]
关键术语:单一职责、展示组件、容器组件、复合组件、组合模式
交互策略:
- 把现有大组件展示给 AI
- 让它分析可拆分的边界
- 讨论拆分方案的利弊
- 逐步重构
避坑指南
- 不要为了拆分而拆分:过度拆分增加理解成本
- 保持命名一致:
UserCard、UserCardContainer、useUserCard - 考虑组件的消费者:API 设计要符合直觉
- 避免 Props 爆炸:超过 7-8 个 Props 考虑重新设计
验收清单
- [ ] 每个组件有清晰的单一职责
- [ ] 展示逻辑和业务逻辑适当分离
- [ ] 使用组合模式实现灵活性
- [ ] 文件组织清晰合理
