项目三:React Hooks 与组件库工程化
从业务 Hooks 抽象到多格式构建、文档示例、测试和版本发布,整理一个可复用 React 基建项目。
项目三:React Hooks 与组件库工程化
资料中的项目是一个带 Demo 官网的 React Hooks 库,包含 TypeScript、Jest、gulp/webpack 构建、UMD 发布、CHANGELOG 和 CI。本文将它改写成可实际交付的公共基建项目,并补充 Hooks 的闭包、取消、竞态和 API 兼容边界。组件库的通用规范另见 前端组件库工程化。
1. 背景与目标
多个 React 业务会重复实现请求状态、分页、轮询、事件监听、媒体查询、键盘交互和表单逻辑。复制代码短期快,长期会出现:状态语义不一致、卸载后仍更新、请求竞态覆盖新数据、测试难以复用、修复无法同步到其他项目。
项目目标不是“封装越多越好”,而是建立一组边界清晰、可测试、可发布的 Hooks,并让消费者知道每个 Hook 的生命周期和失败语义:
- Hook 只封装稳定的状态/副作用协议,不把某个页面的业务字段硬编码进去。
- 返回值和错误语义稳定,受控参数与默认参数不在运行中无提示切换。
- 网络请求支持取消、去重、重试和过期结果丢弃。
- 文档 Demo、类型声明、测试和发布包来自同一提交。
- 使用者可以按包或入口按需引入,不因为一个 Hook 引入整套运行时。
2. 包与仓库结构
packages/
hooks-core/ 纯状态机和公共类型
hooks-dom/ 浏览器事件、媒体查询和 DOM 适配器
hooks-request/ 请求状态、缓存、取消和重试
react-hooks/ React 绑定层
docs/ Demo、API 和迁移文档
tests/ fixture、契约和浏览器交互测试
可以使用 pnpm workspace/monorepo 管理包,但每个包都要声明自己的依赖和公开入口。构建同时产出 ESM、CJS/UMD(只有确有兼容需求时)和 .d.ts;CSS 或 polyfill 等副作用在 package.json 的 sideEffects 中准确声明。
{
"name": "@acme/react-hooks",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./use-request": {
"types": "./dist/use-request.d.ts",
"import": "./dist/use-request.js",
"require": "./dist/use-request.cjs"
}
},
"peerDependencies": { "react": ">=18" },
"sideEffects": false
}
不要让消费者依赖 src/internal 深层路径;需要实验能力时使用带明确稳定性标记的入口。构建工具(gulp、webpack、Vite 等)是实现细节,面试中应能说清入口、依赖图、产物和验证,而不是只报工具名称。
3. Hook 设计方法
先写状态机和不变量,再设计函数签名。例如请求 Hook:
idle -> loading -> success
\-> error(retryable | terminal)
loading -> cancelled
success -> loading (manual refetch / stale revalidation)
一个可解释的接口可以是:
type RequestState<T> = {
data?: T
error?: unknown
status: 'idle' | 'loading' | 'success' | 'error' | 'cancelled'
}
type RequestOptions<T> = {
key: string
fetcher: (signal: AbortSignal) => Promise<T>
enabled?: boolean
retry?: number
retryDelay?: (attempt: number) => number
onError?: (error: unknown) => void
}
function useRequest<T>(options: RequestOptions<T>): RequestState<T> & {
refetch(): void
cancel(): void
} {
// 实现中用 ref 保存当前请求和最新回调,cleanup 时 abort。
throw new Error('example contract')
}
实现时要注意:
- 每次请求生成递增版本或
requestId,只有当前请求能提交结果;旧响应到达时丢弃。 AbortController在依赖变化和组件卸载时调用;取消是预期分支,不应触发错误提示或自动重试。- 重试只针对明确的幂等/临时错误,采用上限和退避,不能让页面卸载后的定时器继续运行。
fetcher、onError等回调可能变化,用 ref 保存最新值,避免 stale closure;依赖数组仍要表达真正影响请求的参数。enabled=false时不发请求;从禁用切换到启用的行为写进文档和测试。
3.1 插件式请求能力
资料提到错误重试、轮询、自动/手动请求、防抖和节流。不要把这些开关堆成互相冲突的布尔值,可拆为可组合策略:
type RequestPlugin<T> = {
onStart?(ctx: RequestContext<T>): void
onSuccess?(ctx: RequestContext<T>): void
onError?(ctx: RequestContext<T>): 'retry' | 'stop' | void
onSettled?(ctx: RequestContext<T>): void
}
轮询需要处理页面隐藏、网络离线和并发:下一次轮询必须在前一次完成后计时,或明确允许并行并设置上限;回到页面时可重新验证。防抖适合搜索提交,节流适合滚动/resize,二者都要在卸载时清理定时器并保留最后一次参数的语义。
3.2 浏览器与 DOM Hook
useEventListener、useIntersectionObserver、useMediaQuery 等 Hook 应把能力检测、监听器选项和清理写入契约:
- SSR 阶段不能访问
window/document,返回明确的初始值。 passive、捕获阶段和目标元素变化需有测试;元素被替换时解绑旧监听器。- Observer/Worker/对象 URL 等资源在 cleanup 中释放。
- 事件回调访问最新状态时避免每次渲染重绑,或清楚说明重绑成本。
4. React 运行模型与常见坑
Hooks 按调用顺序挂在 Fiber 节点的链表上,条件调用会破坏对应关系;“Rules of Hooks”是运行时数据结构约束,不只是风格规范。闭包使每次渲染都捕获自己的值,因此:
setState(value)连续调用可能基于同一个旧快照;需要累积时使用函数式更新。- Effect 中的请求、定时器和事件监听器必须处理依赖、取消和 cleanup。
useMemo/useCallback不是万能性能开关,只有在计算昂贵或引用稳定能减少实际渲染时才有意义。- Context 值对象每次创建会让所有消费者重渲染,必要时拆分上下文或按选择器订阅。
- Strict Mode 开发环境可能重复执行某些初始化,副作用必须幂等。
预测资料还提到 React 19 的 Actions、useTransition、useOptimistic、useFormStatus、use 和错误回调。项目中采用这些能力时要锁定 React 版本、说明是否需要渐进降级,并用 onRecoverableError 等入口关联监控;不要把实验 API 写成所有消费者都可用的硬依赖。状态库(Redux、Zustand、Jotai、Context 等)按状态所有权、调试、异步和订阅粒度选择,不能用“某库一定更快”作为结论。
深入阅读:React Hooks 理解、React 状态与性能、React 组件性能优化。
5. 类型、文档和 Demo
公共 Hook 的类型要表达输入约束和返回状态,而不是用 any 逃避不确定性:
- 泛型参数应能从
fetcher或初始值推断;错误类型若无法约束,文档说明unknown的处理方式。 - 可选配置有默认值,默认行为写入 API 表格;破坏性修改要有迁移示例。
- Demo 覆盖受控/非受控、加载、错误、取消、空值和键盘交互,不只展示成功路径。
- 文档站点和 npm 包从同一版本构建,示例导入公开入口,避免官网能运行而发布包缺文件。
- 在线 CodeSandbox 等第三方环境只能作为演示,不能替代本地 CI 和锁定依赖的回归测试。
6. 测试策略
测试按风险分层:
| 层级 | 覆盖内容 | 关键 fixture |
|---|---|---|
| 纯函数/状态机 | 重试次数、退避、状态转移、去重 | 可控时钟、固定响应序列 |
| Hook 渲染测试 | 首次加载、依赖变化、卸载、Strict Mode | mock fetcher、AbortSignal 断言 |
| 组件交互 | 键盘、焦点、错误和异步反馈 | 可访问定位器、稳定 DOM |
| 包契约 | ESM/CJS/UMD、类型声明、exports | 干净临时消费者项目 |
| 浏览器 E2E | 文档导航、Demo、发布包真实导入 | 隔离网络和版本 fixture |
每个异步测试都等待可观察状态,而不是 sleep 固定时间。故意让 fetcher 延迟、拒绝、乱序和取消,验证旧结果不会覆盖新结果。可以用 mutation 或小缺陷注入确认测试真正能阻止回归。
7. 构建、发布和版本治理
提交 -> 类型检查/Lint -> 单元/交互测试 -> 构建 ESM/CJS/UMD + d.ts
-> 包内容/exports 检查 -> 生成 changelog -> 预发布验证
-> npm/内部仓库发布 -> 文档/CDN 同步 -> 监控与回滚
发布要点:
- 使用语义化版本;删除 Hook、改变默认重试或改变返回状态属于破坏性变更,应提供迁移窗口。
- 同一个版本号不能覆盖不同产物;记录 commit、构建器版本和依赖锁文件。
- UMD 只在确有旧系统或 CDN 需求时保留,默认优先 ESM tree-shaking;检查重复 React、全局变量和 sourcemap 泄露。
- CHANGELOG 自动化只能生成草稿,最终内容要人工确认影响范围和迁移步骤。
- 发布失败或消费者安装失败时,保留上一稳定版本和撤回入口,不依赖“重新发一次同版本”。
8. 失败边界与复盘
| 失败 | 根因示例 | 预防/止血 |
|---|---|---|
| 旧请求覆盖新请求 | 没有 requestId 或 abort | 当前请求版本校验、取消和乱序测试 |
| 卸载后 setState | Effect cleanup 缺失 | 统一资源管理 helper、卸载测试 |
| 生产包缺少类型/样式 | exports 或 sideEffects 配置错误 | 干净消费者安装检查、产物清单 |
| Hook API 破坏消费者 | 默认值/返回形状无版本治理 | 语义化版本、弃用警告和迁移文档 |
| 官网 Demo 与包不一致 | 文档和包使用不同提交 | 同一 CI 产出、锁定依赖和发布 smoke test |
| 自动重试放大故障 | 未区分取消、429 和业务错误 | 幂等判断、退避、上限、熔断开关 |
复盘时记录“哪个不变量被破坏、哪条测试没有覆盖、怎样在发布前捕获”,不要只写“加强测试”。
9. 指标与证据
项目价值可从以下维度证明:
| 维度 | 指标定义 | 证据来源 |
|---|---|---|
| 复用 | 真实消费者数、重复逻辑删除量、公开 API 使用率 | 依赖清单、代码搜索、迁移 PR |
| 可靠性 | Hook 相关回归缺陷、取消/重试成功率、flaky 测试率 | CI、错误监控、测试报告 |
| 交付 | 从提交到发布耗时、构建失败原因、回滚次数 | CI/CD 日志 |
| 产物 | ESM/UMD 包大小、重复依赖、类型检查通过率 | bundle 分析和消费者 smoke test |
| 体验 | Demo 加载、文档搜索/反馈、组件可访问性缺陷 | 浏览器 trace、反馈和审计 |
任何“复用提升”“首屏提升”等数字都要带版本、样本和测量方法;没有记录就写待验证,不引用资料中的示例百分比。
10. 面试表达与追问
三分钟版本
团队多个 React 项目重复实现请求、轮询和 DOM 逻辑,复制代码导致取消、竞态和版本问题。
我负责 Hooks API、请求状态机、测试和发布链路,按 hooks-core、浏览器适配和 request 包拆边界,产出 ESM/CJS/类型声明并提供可运行 Demo。
请求 Hook 用 requestId/AbortController 丢弃旧响应,重试只覆盖幂等临时错误;Effect cleanup、SSR 能力检测和 Strict Mode 都有回归用例。
CI 从类型/Lint 到包内容和消费者安装,发布带 changelog、版本和回滚入口;结果用真实消费者、缺陷和构建记录证明。
高频追问
| 追问 | 回答要点 | 深入阅读 |
|---|---|---|
| 为什么要封装 Hook,而不是复制工具函数? | Hook 能把状态、生命周期和副作用契约一起复用;纯函数仍应抽到 core,避免把所有逻辑绑在 React | React Hooks |
| 如何处理请求竞态? | 每次请求有 ID/版本,只有当前请求提交;切换和卸载 abort;测试乱序响应 | AI 前端工程 |
| 防抖和节流放在哪里? | 纯函数层可复用,Hook 层负责生命周期和最新回调;说明输入/尾调用/取消语义 | JavaScript 高频追问 |
| 为什么要出 UMD? | 只有旧系统/CDN 需要;现代应用优先 ESM,比较包体、全局污染和缓存粒度 | 组件库工程化 |
| 如何保证发布包可用? | exports、d.ts、ESM/CJS/UMD、peerDependencies 和 CSS 副作用做干净消费者检查 | 组件库工程化 |
| React 19 能直接使用吗? | 先锁版本和消费者范围,给渐进降级;错误回调和新 Hook 要有版本/测试边界 | React 面试真题补充 |
11. 交付清单
- 每个 Hook 有状态机、参数/返回契约和失败语义。
- 取消、竞态、卸载、SSR、Strict Mode 和高频事件有测试。
- 包边界、exports、类型声明、peerDependencies 和副作用经过消费者验证。
- 文档 Demo 与发布包来自同一提交,包含错误/空态/键盘示例。
- CI 有类型、Lint、单元、交互、构建和包内容检查,发布可回滚。
- 结果指标带版本、样本、基线和测量方法。
来源:高级前端亮点项目.pdf 第 25-27 页(React Hooks 库、TypeScript、Jest、gulp/webpack、UMD、Dumi、Demo 和 CI 发布);高频真题解析与9月考点预测上.pdf 第 15-16 页(Hooks/Fiber、状态管理和 React 19);高频真题解析与9月考点预测中.pdf 第 2-9 页(防抖/节流、主题与性能指标)。