一句话定义
把全知识库的知识压缩成一份可执行的审查清单:每一条反模式都指向具体知识点,每一条检查项都能在 30 秒内对一段 PR 做出判断——这是从「学会」到「持续做对」的护栏。
为什么重要
知识会遗忘,清单不会。React 的错误高度模式化(就是那十几种),review 时按清单扫一遍能拦截 80% 的典型问题;团队把它配置成 lint 规则与 PR 模板后,还能自动化一半。
前置知识
本篇是全库的收束,建议至少完成第二、三阶段后再用作工具。
核心概念:十大反模式速查
| # | 反模式 | 症状 | 正解 | 出处 |
|---|---|---|---|---|
| 1 | 渲染期间做副作用 | 组件体里 fetch/DOM/订阅 | effect 或事件 | kp-010 |
| 2 | 用 effect 做派生 | useEffect(() => setTotal(a+b)) | 渲染时直接算 | kp-005/010 |
| 3 | 依赖数组撒谎 | 删依赖「修好」stale bug | 诚实依赖或重构 | kp-014 |
| 4 | 原地变异 state | push/splice 后界面不动 | 不可变更新 | kp-005 |
| 5 | index 作 key | 插入/排序后输入错位 | 数据 id | kp-009 |
| 6 | 无脑全量记忆化 | 到处 useMemo/useCallback | 先测量 | kp-018/019 |
| 7 | 巨型 Context / 服务端数据进 Context | 全树重渲染 | 拆分/状态库/数据层 | kp-012/021 |
| 8 | 布尔 prop 开关地狱 | size closable dark hoverable… | children 组合 | kp-013 |
| 9 | 手写三态数据组件 | loading/error/data ×N | 数据层 + Suspense | kp-020/021 |
| 10 | 渲染中读可变外部值 | Date.now()/random/store | 快照进 state / useSyncExternalStore | kp-022 |
原理 / 机制:完整审查清单
结构与状态(审查 2 分钟)
- state 只存「不可派生」的值;派生值(合计、过滤、格式化)在渲染时计算?(kp-005)
- 状态分型正确:API 数据走数据层、跨组件客户端状态走 store、其余 useState?(kp-021)
- 组件是纯函数:渲染期间无请求/无 DOM/无外部写?(kp-003/010)
- 大状态对象已拆分或用 reducer 集中,而非
setForm({...form, ...patch})撒胡椒面?(kp-016)
Hooks(审查 3 分钟)
- 没有 Hook 在 if/循环/回调中;lint 的 rules-of-hooks 已开启?(kp-014)
- effect 只做「与渲染系统同步」的事;事件驱动的逻辑在事件处理器里?(kp-010)
- 每个 effect 有对称清理;请求有竞态守卫(ignore/AbortController)?(kp-010)
- 依赖数组完整;没有为「少跑一次」而删除依赖?(kp-014)
- 跨渲染可变值用 ref 且只在事件/effect 中读写?(kp-017)
渲染与性能(审查 2 分钟)
- 列表 key 来自稳定数据 id?(kp-009)
- memo/useMemo/useCallback 有 Profiler 证据支撑,而非「以防万一」?(kp-018/019)
- Context value 有 useMemo;Context 按领域拆分?(kp-012)
- 长列表(>100 行)用虚拟化而非纯 memo?(kp-019)
- 重型组件/路由已 lazy + Suspense?(kp-019/020)
结构与 API 设计(审查 2 分钟)
- 配置式 props(>4 个布尔/枚举)已改组合?(kp-013)
- 'use client' 边界下沉到叶子,而非整页/整分支?(kp-024/026)
- props 只读:没有修改传入对象/数组?(kp-004)
- 事件传函数引用而非调用结果:
onClick={fn}vsonClick={() => fn(id)}?(kp-006)
可访问性与错误(审查 1 分钟)
- 交互元素语义正确(button 不是 div+onClick);图标按钮有 aria-label?(kp-006)
- 异步分支有 Error Boundary;用户能看到错误而不是白屏?(kp-020)
- 表单提交走 Actions 或受控且有校验路径?(kp-011/025)
测试(审查 1 分钟)
- 新的纯逻辑(reducer/工具/Hook)有单元测试?(kp-027)
- 组件测试断言用户可见行为而非实现细节?(kp-027)
直观类比
审查清单像飞行员起飞检查单:不是不信任机长的技术,而是承认「人会在疲劳时漏项」。每一条都是真实事故换来的——index key 造成的购物车串行、撒谎依赖造成的「偶现 bug」、巨型 Context 造成的全站卡顿。清单的价值不在条目本身,而在每次都过一遍这个动作。
实例 / 案例
把清单落地为团队机制的三层防线:
text1. 自动层:eslint-plugin-react-hooks(exhaustive-deps + rules-of-hooks)
typescript-eslint(no-floating-promises)
→ PR 阶段机器拦截,人类只看机器看不见的
2. 流程层:PR 模板内嵌本清单(10 分钟能过完的粒度)
review 评论直接引用条目编号,如 "kp-009: key 请用数据 id"
3. 文化层:每个季度把高频踩中的条目做成 15 分钟内部分享
→ 条目递减,清单「越用越短」
一个真实 review 片段的示范:
jsx// PR diff
+ useEffect(() => {
+ if (!user) return;
+ api.logView(user.id, page);
+ }, [user, page]);
// review 意见(引用清单):
// ⚠️ kp-010: 依赖完整 ✅ 但缺竞态守卫——page 快速切换时旧请求可能后到。
// 建议 AbortController 或 ignore 标志。
// ⚠️ kp-010: 埋点若由「用户操作」触发(点击导航),更适合放事件处理器。
常见误区
- 把清单当考试而靄件工具:逐字背诵毫无意义;用法是「review 时开着它」。
- 清单正确但依赖错误:lint 全绿 ≠ 无问题——清单里有一半是机器查不了的(设计、分型、粒度)。
- 追求零反模式的洁癖:原型期的代码允许欠债,但欠债要记账(TODO + 关联 KP 编号);「知道自己在欠债」与「不知道」是两种工程师。
- 只查新代码:每周一次对热点文件(改动最频繁的 10 个文件)跑一遍清单,收益最高。
自测题
- 不看上文,默写十大反模式中的任意六个及对应正解。
- 用清单审查下面代码,列出所有问题及编号:
``jsx function Cart({ items }) { const [total, setTotal] = useState(0); useEffect(() => { setTotal(items.reduce((s, i) => s + i.price, 0)); }, [items]); items.sort((a, b) => a.price - b.price); return ( <ul> {items.map((it, i) => ( <li key={i} onClick={() => console.log(it.id)}>{it.name}</li> ))} <b>{total}</b> </ul> ); } `` (答案线索:#2 派生进 effect、#4 sort 原地变异 props、#5 index key、li 可访问性、total 应用渲染派生。)
- 三层防线各拦截哪类问题?为什么「lint 全绿」不等于安全?
- 为你的下一个项目从清单中挑出 5 条「必查项」并说明理由。
与其他知识点的关系
- 向前:本篇是 kp-004~026 全部知识的运维形态——清单每条都回链。
- 向后:学习路径的「检验标准」第三问(能否讲清为什么)以本篇为标尺;后续可把清单条目逐步固化为团队 lint 规则与 PR 模板。
延伸阅读
- You Might Not Need an Effect(官方反模式第一手材料):<https://react.dev/learn/you-might-not-need-an-effect>
- Common mistakes(官方博客系列):<https://react.dev/learn/common-mistakes>