组件是 React 应用的基本构建单元,组件设计的好坏直接影响项目的可维护性、可测试性和团队协作效率。遵循清晰的设计规范,能让代码在项目规模扩大时依然保持可控。
20.3.1 单一职责:一个组件只做一件事
单一职责原则要求每个组件只负责一个明确的功能。当一个组件开始同时处理数据获取、UI 渲染和业务逻辑时,它就会变得难以理解、难以复用、难以测试。
如何判断组件是否违反单一职责?
- 组件内部有多个互不相关的 state 或副作用。
- 组件代码超过了 200 行(经验阈值,不是绝对标准)。
- 你很难用一句话描述这个组件是做什么的。
实例:拆解一个违规组件
❌ 一个 UserDashboard 组件既请求数据,又渲染用户信息,还处理数据过滤:
function UserDashboard() {
const [users, setUsers] = useState([]);
const [search, setSearch] = useState('');
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
const filteredUsers = users.filter(u => u.name.includes(search));
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<ul>
{filteredUsers.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
</div>
);
}
✅ 遵循单一职责的拆分:
// 数据请求 Hook
function useUsers() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(res => res.json()).then(setUsers);
}, []);
return users;
}
// 搜索输入组件
function SearchBar({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} placeholder="搜索用户" />;
}
// 用户列表组件
function UserList({ users }) {
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
// 容器组件只负责组合
function UserDashboard() {
const users = useUsers();
const [search, setSearch] = useState('');
const filtered = users.filter(u => u.name.includes(search));
return (
<div>
<SearchBar value={search} onChange={setSearch} />
<UserList users={filtered} />
</div>
);
}
拆分后每个部分都可以独立开发和测试,SearchBar 可以在任何需要搜索的地方复用。
20.3.2 粒度拆分:把握组件大小与边界
粒度拆分需要在“太碎”和“太大”之间找到平衡。过于细碎的组件会导致文件数量爆炸、Props 传递链条过长;过于庞大的组件又会违反单一职责。
拆分原则:
- 视图层拆分:当一段 JSX 有明显的语义独立块(如页面头部、侧边栏、列表项),提取为独立组件。
- 逻辑层拆分:将复杂的计算、状态管理、副作用封装到自定义 Hooks 中,让组件只负责粘合。
- 按变更频率拆分:经常变化的部分与稳定的部分分离,减少改动影响范围。
粒度决策实例
假设有一个 ProductCard 组件,包含商品图片、名称、价格和“加入购物车”按钮。当这个组件内部还包含了库存状态计算、促销标签逻辑、点击埋点时,它其实可以拆分为:
ProductImage:纯展示ProductInfo:名称 + 价格AddToCartButton:按钮交互useProductStock:库存逻辑 HookusePromotionLabel:促销标签计算 Hook
最终 ProductCard 只是这些组件的组合容器。每个子组件都可独立测试,AddToCartButton 还可以在其他地方复用。
避免过度拆分:如果某个元素只在一个地方使用,且逻辑简单,没必要单独抽离成一个文件(如一个只包含一个 <hr> 的 Divider 组件,除非它承载了全局样式设计意图)。
20.3.3 命名规范:表意清晰、统一风格
命名是代码可读性的第一要素。React 组件和其相关文件的命名应遵循团队达成一致的约定。
组件命名
- 使用 PascalCase(大驼峰)命名组件:
UserList、ProductCard、AppHeader - 组件名应与文件名一致:文件
UserList.tsx导出UserList - 名字要能直观反映其 UI 角色或业务含义,避免模糊命名(如
Box、Item),除非是通用布局组件 - 对于高阶组件(HOC),使用
withSomething前缀:withAuth、withTheme
Props 命名
- 使用描述性名称,布尔类型 Props 常用
is/has/should前缀:isActive、hasError、shouldDisplay - 事件回调 Props 使用
on前缀:onClick、onSubmit、onChange(与原生事件一致) - 避免将 HTML 属性名用作 Props 除非刻意传递:比如不要命名一个 Prop 为
className除非你真的打算覆盖 CSS 类名
Hooks 命名
- 必须以
use开头:useUsers、useLocalStorage、useDebounce - 名字应描述其功能,而非实现细节
文件与目录结构
- 组件文件可以采用
PascalCase命名:UserList.tsx、ProductCard.tsx - 一个组件可以是一个文件夹,内部包含
index.tsx、style.module.css、test.tsx,便于管理同组件附属资源 - 页面级组件可以放在
pages/或screens/,通用 UI 组件放在components/,业务组件按功能域分目录
示例:规范化的组件目录
src/
components/
UserList/
UserList.tsx
UserList.test.tsx
UserList.module.css
index.ts
SearchBar.tsx
hooks/
useUsers.ts
UserList 文件夹通过 index.ts 统一导出,外部引入时路径简洁:
// index.ts
export { UserList } from './UserList';
// 使用
import { UserList } from '@/components/UserList';
规范的最终目的是提高认知效率。任何命名都应该让新加入的团队成员能在几秒钟内推断出它的大致作用,而不需要深入阅读代码。
小结
组件设计规范不是僵化的教条,而是帮助团队写出可维护代码的指导方针。遵循单一职责让组件保持专注,合理的粒度拆分平衡复用与复杂度,清晰的命名和结构让代码自文档化。在实际项目中,可以逐步应用这些原则,并通过代码评审不断强化,最终形成团队共识。