4、组件开发规范:命名规范、Props设计原则、组件单一职责、受控与非受控组件
好,咱们今天聊聊组件开发的「规矩」。
你可能觉得,写组件嘛,能跑就行。但我在团队里带过不少新人,发现一个规律:代码烂不烂,不看功能实现,看组件设计。功能跑通只是及格线,组件设计得好,后面维护的人会感谢你;设计得烂,三个月后你自己都想骂娘。
这一章,我把几个核心原则掰开揉碎讲清楚。你照着做,至少能少踩一半的坑。
4.1 命名规范:让代码自己会说话
命名这件事,我吃过不少亏。以前有个项目,组件名叫 Box,结果里面塞了表单、表格、弹窗……后来没人敢动它,因为谁都不知道改了会不会炸。
命名不是给机器看的,是给人看的。好的命名,看一眼就知道这个组件是干嘛的。
核心原则:组件名 = 用途 + 类型
我习惯用 PascalCase(大驼峰)命名组件文件。举个例子:
// ✅ 好名字
UserAvatar.tsx
PaymentForm.tsx
DataTable.tsx
// ❌ 坏名字
Box.tsx // 太抽象,不知道装什么
Utils.tsx // 这不是组件,是垃圾桶
index.tsx // 到处都是,鬼知道是哪个
还有 Props 的命名,也有讲究。我个人建议:
- 事件回调用 on 开头:
onClick、onChange、onSubmit - 布尔值用 is/has/should 开头:
isLoading、hasError、shouldShow - 配置项用语义化名称:
variant、size、placement
小技巧:命名时问自己一句——「如果别人不看文档,能猜出这个 prop 是干嘛的吗?」如果答案是否定的,那就改。
4.2 Props 设计原则:少即是多
Props 是组件的「接口」。接口设计得烂,调用方就痛苦。
我记得有一次,一个同事写了个 Button 组件,Props 有 30 多个。什么 color、bgColor、borderColor、hoverColor…… 你想想看,调用的时候得传多少参数?
我总结了几条 Props 设计原则:
4.2.1 保持 Props 数量精简
一个组件超过 10 个 Props,基本就该拆了。不是功能多就好,而是职责要单一。
4.2.2 提供合理的默认值
能默认的,就别让调用方传。比如按钮的 type 默认 'button',而不是让用户每次都写。
// ✅ 好的设计
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
// ❌ 坏的设计
interface ButtonProps {
color: string;
backgroundColor: string;
borderColor: string;
hoverColor: string;
// ... 还有 20 个
}
4.2.3 避免 Props 冲突
如果你要透传原生 HTML 属性,用 ...rest 或者 ComponentPropsWithoutRef。别自己重新定义一遍 id、className 这些。
注意:我曾经见过有人把 style 也放到 Props 里重新定义,结果和原生 style 冲突,调试了一下午。嗯,这种坑踩过一次就记住了。
4.3 组件单一职责:一个组件只做一件事
「单一职责」这个词你可能听过。说白了就是:如果一个组件有多个「变」的理由,就该拆。
举个例子,一个 UserCard 组件,它既展示用户信息,又处理编辑逻辑,还负责发送消息…… 这就乱套了。
我一般这样拆分:
- 展示组件:只负责渲染,不关心数据来源。比如
UserAvatar、UserName - 容器组件:负责获取数据、管理状态。比如
UserProfileContainer - 业务组件:组合多个展示组件,完成一个业务功能。比如
UserCard
你想想看,如果每个组件只做一件事,测试也好写,复用也方便,改起来也不怕牵连。
判断标准:当你给组件加新功能时,如果觉得「这个功能放这里有点怪」,那就说明它该拆了。
4.4 受控与非受控组件:选对模式,少写 Bug
这个话题,面试常问,实际开发也天天用。我简单说清楚。
4.4.1 什么是受控组件?
说白了,就是「数据由 React 状态管理」。用户输入 -> 触发 onChange -> 更新 state -> 重新渲染。数据流是单向的,清晰可控。
// 受控组件示例
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
4.4.2 什么是非受控组件?
数据由 DOM 自己管,React 不插手。用 ref 去拿值。
// 非受控组件示例
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => {
console.log(inputRef.current.value);
};
return <input ref={inputRef} />;
}
4.4.3 什么时候用哪个?
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 表单验证、实时联动 | 受控 | 需要实时获取值做校验或联动 |
| 文件上传 | 非受控 | 文件对象由浏览器管理,React 管不了 |
| 简单输入框(无校验) | 非受控 | 减少渲染次数,性能更好 |
| 需要重置/设置值 | 受控 | 非受控组件重置麻烦,得操作 DOM |
我的习惯:默认用受控。除非明确知道「这里不需要 React 管」,才用非受控。因为受控组件的行为更可预测,调试起来也省心。
4.4.4 组件库里的受控与非受控
在组件库中,我建议支持「受控 + 非受控」两种模式。怎么实现?用 useControllableState 这个模式。
// 一个支持受控和非受控的组件
function Counter({ value: controlledValue, defaultValue, onChange }) {
const [internalValue, setInternalValue] = useState(defaultValue ?? 0);
// 判断是否受控
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
const handleChange = (newValue) => {
if (!isControlled) {
setInternalValue(newValue);
}
onChange?.(newValue);
};
return (
<div>
<button onClick={() => handleChange(value - 1)}>-</button>
<span>{value}</span>
<button onClick={() => handleChange(value + 1)}>+</button>
</div>
);
}
这样,调用方可以自由选择:
// 非受控用法
<Counter defaultValue={0} />
// 受控用法
<Counter value={count} onChange={setCount} />
避坑指南:我曾经在组件里同时用了 value 和 defaultValue,结果用户传了 value 但没传 onChange,组件直接「死」了。记住:受控组件必须同时提供 value 和 onChange,否则用户改不了值,会以为是 Bug。
4.5 小结
这一章的内容,说白了就是四个字:约定大于配置。
命名规范让代码可读,Props 设计让接口易用,单一职责让逻辑清晰,受控非受控让数据可控。这些不是花架子,是实打实能减少 Bug 的。
下一章,咱们聊聊组件的样式方案。嗯,那又是一个大坑,到时候我慢慢给你讲。