4、组件开发规范:命名规范、Props设计原则、组件单一职责、受控与非受控组件

好,咱们今天聊聊组件开发的「规矩」。

你可能觉得,写组件嘛,能跑就行。但我在团队里带过不少新人,发现一个规律:代码烂不烂,不看功能实现,看组件设计。功能跑通只是及格线,组件设计得好,后面维护的人会感谢你;设计得烂,三个月后你自己都想骂娘。

这一章,我把几个核心原则掰开揉碎讲清楚。你照着做,至少能少踩一半的坑。

4.1 命名规范:让代码自己会说话

命名这件事,我吃过不少亏。以前有个项目,组件名叫 Box,结果里面塞了表单、表格、弹窗……后来没人敢动它,因为谁都不知道改了会不会炸。

命名不是给机器看的,是给人看的。好的命名,看一眼就知道这个组件是干嘛的。

核心原则:组件名 = 用途 + 类型

我习惯用 PascalCase(大驼峰)命名组件文件。举个例子:

// ✅ 好名字
UserAvatar.tsx
PaymentForm.tsx
DataTable.tsx

// ❌ 坏名字
Box.tsx        // 太抽象,不知道装什么
Utils.tsx      // 这不是组件,是垃圾桶
index.tsx      // 到处都是,鬼知道是哪个

还有 Props 的命名,也有讲究。我个人建议:

  • 事件回调用 on 开头onClickonChangeonSubmit
  • 布尔值用 is/has/should 开头isLoadinghasErrorshouldShow
  • 配置项用语义化名称variantsizeplacement

小技巧:命名时问自己一句——「如果别人不看文档,能猜出这个 prop 是干嘛的吗?」如果答案是否定的,那就改。

4.2 Props 设计原则:少即是多

Props 是组件的「接口」。接口设计得烂,调用方就痛苦。

我记得有一次,一个同事写了个 Button 组件,Props 有 30 多个。什么 colorbgColorborderColorhoverColor…… 你想想看,调用的时候得传多少参数?

我总结了几条 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。别自己重新定义一遍 idclassName 这些。

注意:我曾经见过有人把 style 也放到 Props 里重新定义,结果和原生 style 冲突,调试了一下午。嗯,这种坑踩过一次就记住了。

4.3 组件单一职责:一个组件只做一件事

「单一职责」这个词你可能听过。说白了就是:如果一个组件有多个「变」的理由,就该拆

举个例子,一个 UserCard 组件,它既展示用户信息,又处理编辑逻辑,还负责发送消息…… 这就乱套了。

我一般这样拆分:

  • 展示组件:只负责渲染,不关心数据来源。比如 UserAvatarUserName
  • 容器组件:负责获取数据、管理状态。比如 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} />

避坑指南:我曾经在组件里同时用了 valuedefaultValue,结果用户传了 value 但没传 onChange,组件直接「死」了。记住:受控组件必须同时提供 valueonChange,否则用户改不了值,会以为是 Bug。

4.5 小结

这一章的内容,说白了就是四个字:约定大于配置

命名规范让代码可读,Props 设计让接口易用,单一职责让逻辑清晰,受控非受控让数据可控。这些不是花架子,是实打实能减少 Bug 的。

下一章,咱们聊聊组件的样式方案。嗯,那又是一个大坑,到时候我慢慢给你讲。