组件设计与复用:写一次到处用
props 设计、children 组合、自定义 Hook、受控表单与 key
- 会设计合理的组件 props
- 理解 children 组合 vs 配置化
- 知道自定义 Hook 抽什么、和工具函数差在哪
- 理解受控组件与列表 key 的作用
组件就像积木:大积木套小积木,props 是每块积木对外的插口,children 是「这块积木中间留个位置让别人塞内容」。自定义 Hook 则像把一段经常重复的逻辑打包成一个函数,哪里要用就 import 一下。
props 设计:让组件好复用
好组件的 props 要单一、清晰、有默认值。能传内容就别写死,能给默认值就别让外部每次都传。
// 一个可复用的按钮: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 允许父组件把任意内容塞进组件的「预留位置」。这让组件不需要穷举所有变体,而是把骨架固定、内容开放。
// 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,普通工具函数不能。
// 自定义 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。过早抽象比重复代码更难维护——只有一个组件用的逻辑,先写在组件里,等第二次出现时再抽,几乎不会错。
受控表单与列表 key
受控组件:表单输入框的值由 React state 控制(value 绑定 state,onChange 更新 state),这样数据和界面永远一致。列表渲染时每个元素要给一个稳定的 key,React 靠它识别哪一项是新增、删除还是移动。
// 受控输入框:值来自 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} />)}列表会增删排序时,用 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 和普通工具函数的关键区别是?
资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
网络侧:DNS→TCP/TLS→HTTP 请求与响应;浏览器侧:解析 HTML 建 DOM、解析 CSS 建 CSSOM、合并成渲染树,再经过布局(Layout)、绘制(Paint)、合成(Composite)上屏。前端性能优化几乎都围绕这条链路:减小资源体积、优化关键渲染路径、避免频繁触发重排重绘。
挑战任务
props 带默认值
可复用组件的 props 常带默认值。实现 button_props(props):返回按钮文字 label,若 props 里没给就用默认「点击」。
课后作业
判断输入框是不是正确受控
受控组件必须同时给 value 和 onChange:只给 value 不给 onChange,输入框会被锁死敲不进去。实现 is_controlled(props):props 字典里同时含 value 和 onChange 才返回 True。打印三个用例。