12 分钟
全栈开发

组件设计与复用:写一次到处用

props 设计、children 组合、自定义 Hook、受控表单与 key

  • 会设计合理的组件 props
  • 理解 children 组合 vs 配置化
  • 知道自定义 Hook 抽什么、和工具函数差在哪
  • 理解受控组件与列表 key 的作用
💡高中搭桥

组件就像积木:大积木套小积木,props 是每块积木对外的插口,children 是「这块积木中间留个位置让别人塞内容」。自定义 Hook 则像把一段经常重复的逻辑打包成一个函数,哪里要用就 import 一下。

props 设计:让组件好复用

好组件的 props 要单一、清晰、有默认值。能传内容就别写死,能给默认值就别让外部每次都传。

JavaScript
// 一个可复用的按钮:type 决定样式,children 是按钮上的文字
function Button({ type = "primary", children, onClick }) {
  const styles = { primary: "bg-indigo", ghost: "bg-none border" };
  return (
    <button className={styles[type]} onClick={onClick}>
      {children}
    </button>
  );
}

// 用法:同一块积木,塞不同内容
<Button type="primary">保存</Button>
<Button type="ghost">取消</Button>

这里有个真实痛点叫「props 爆炸」:一开始按钮只要 type,后来加了 size、disabled、loading、icon、shape、block……props 列了十几个,每个调用方都要记一遍哪些该传、哪些有默认值。解法不是把 props 列得更长,而是分清哪些用「配置」(简单的开关、枚举值,走 props 合理),哪些用「组合」(内容、布局这种千变万化的,别用 props 穷举,交给 children)。比如按钮右上角要不要个小徽章,与其加个 badgeText/badgeColor 两个 props,不如让调用方自己塞 children 结构。

children:组合优于配置

children 允许父组件把任意内容塞进组件的「预留位置」。这让组件不需要穷举所有变体,而是把骨架固定、内容开放。

JavaScript
// Card 只负责卡片的外框样式,内部长什么由外面塞进来
function Card({ title, children }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}

<Card title="用户信息">
  <Avatar />
  <p>姓名:小明</p>
</Card>

自定义 Hook:抽复用逻辑

当两个组件里有一段相同的状态逻辑(比如都要监听窗口宽度、都要请求同一个接口),就把它抽成一个以 use 开头的函数,这就是自定义 Hook。它能使用 useState/useEffect,普通工具函数不能。

JavaScript
// 自定义 Hook:复用「窗口宽度」逻辑
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  return width;
}

// 任何组件直接用
function Sidebar() {
  const w = useWindowWidth();
  return <div>{w < 768 ? "手机版" : "桌面版"}</div>;
}
💡什么时候才抽自定义 Hook

判断标准:是「同一段带状态/副作用的逻辑」在两个以上组件里复制粘贴了,才抽。如果只是纯数据计算(把名字格式化一下),抽成普通工具函数即可,别硬凑成 Hook。过早抽象比重复代码更难维护——只有一个组件用的逻辑,先写在组件里,等第二次出现时再抽,几乎不会错。

受控表单与列表 key

受控组件:表单输入框的值由 React state 控制(value 绑定 state,onChange 更新 state),这样数据和界面永远一致。列表渲染时每个元素要给一个稳定的 key,React 靠它识别哪一项是新增、删除还是移动。

JavaScript
// 受控输入框:值来自 state,改动更新 state
function LoginForm() {
  const [email, setEmail] = useState("");
  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}

// 列表一定要给稳定 key(用数据 id,别用数组下标)
{users.map(u => <UserRow key={u.id} user={u} />)}
⚠️key 别用数组下标

列表会增删排序时,用 index 当 key 会让 React 错配 DOM、出诡异的 bug。永远用数据里稳定唯一的 id 当 key。

受控组件的对立面是非受控组件:输入框的值不管,让 DOM 自己存,要的时候用 ref 去读。受控的好处是数据和界面永远一致、随时能根据状态改输入框(比如校验后立刻变红、清空);代价是每次按键都触发一次 React 重渲染。踩坑点最常见的是「只给了 value 没给 onChange」——React 会把输入框锁死,因为你声明了要控制它却不更新 state,用户敲不进去。表单简单、不需要实时联动时用非受控更省;需要校验、联动、回显时用受控。

🐍组件设计两条实战原则

第一条:单一职责。一个组件如果既管数据请求、又管数据格式化、又管三四种布局,就该拆开——取数的归自定义 Hook,纯展示的归子组件。第二条:props 只传用得上的。别把整个大对象一股脑传下去让子组件自己挑,这会让子组件依赖一堆它根本不关心的字段,一改就到处崩。传具体需要的那几个原始值,组件才真正可复用。

配上 TypeScript(上一课),props 设计就更稳:每个组件的 props 用 interface 声明清楚,哪些必填、哪些可选、什么类型,编辑器会在你用错时立刻提醒。这也是为什么本项目全是 .tsx——props 有了类型约束,组件之间的契约才不会乱。

一句话总结组件设计:骨架固定、内容开放(children)、props 做有限的开关、重复逻辑抽自定义 Hook。做到这几点,你的组件才真正是「写一次到处用」,而不是每个页面复制粘贴改一改。

选择题

自定义 Hook 和普通工具函数的关键区别是?

填空题填写空白处的代码
React 渲染列表时,每个子元素必须有稳定的 属性,帮助 React 识别哪些项增删了。

资深工程师加餐

底层原理 · 大厂视角 · 工程经验,点卡片展开

网络侧:DNS→TCP/TLS→HTTP 请求与响应;浏览器侧:解析 HTML 建 DOM、解析 CSS 建 CSSOM、合并成渲染树,再经过布局(Layout)、绘制(Paint)、合成(Composite)上屏。前端性能优化几乎都围绕这条链路:减小资源体积、优化关键渲染路径、避免频繁触发重排重绘。

挑战任务

props 带默认值

简单+50 XP

可复用组件的 props 常带默认值。实现 button_props(props):返回按钮文字 label,若 props 里没给就用默认「点击」。

props 带默认值
1 个测试用例

课后作业

判断输入框是不是正确受控

中等+20 XP

受控组件必须同时给 value 和 onChange:只给 value 不给 onChange,输入框会被锁死敲不进去。实现 is_controlled(props):props 字典里同时含 value 和 onChange 才返回 True。打印三个用例。

判断输入框是不是正确受控
1 个测试用例