React 学习站
React›前沿与实践›进阶

反模式与代码审查清单

进阶前沿与实践

一句话定义

把全知识库的知识压缩成一份可执行的审查清单:每一条反模式都指向具体知识点,每一条检查项都能在 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原地变异 statepush/splice 后界面不动不可变更新kp-005
5index 作 key插入/排序后输入错位数据 idkp-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数据层 + Suspensekp-020/021
10渲染中读可变外部值Date.now()/random/store快照进 state / useSyncExternalStorekp-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} vs onClick={() => 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 个文件)跑一遍清单,收益最高。

自测题

  1. 不看上文,默写十大反模式中的任意六个及对应正解。
  2. 用清单审查下面代码,列出所有问题及编号:

``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 应用渲染派生。)

  1. 三层防线各拦截哪类问题?为什么「lint 全绿」不等于安全?
  2. 为你的下一个项目从清单中挑出 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>