合并AI生成的React Hook前检查什么
文章梳理AI生成React Hook的常见问题,包括Effect依赖遗漏、对象引用变化导致重复触发,以及清理逻辑和过度封装。作者建议从业务上应当重新触发的条件出发,审查依赖项与请求生命周期。
AI 深度解读
AI 写出的 React Hook,经常能过类型检查,也能跑通演示,却未必经得住路由切换、连续输入和组件卸载。原文作者给出的合并标准很直接:先检查依赖、状态、清理和副作用,再决定这段抽象值不值得留下。
这份清单的价值,在于把「看起来符合 React 写法」拆成可追问的行为约定。不过,其中也有需要校正的判断:函数闭包不会天然过期,useCallback 也没有失效,只是它们未必解决了眼前的问题。
依赖数组写的是触发条件,不是运行愿望
作者首先检查 useEffect:究竟什么变化应该让它重新执行?
下面这段代码展示了最典型的错误,把读取动态值的请求标成「只在挂载时运行」:
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // 仅在挂载时运行
要盯住的是 []:effect 读取了 userId,却不会因 userId 变化而重新请求。组件继续复用时,页面可能还停留在第一个用户的数据上。
另一种错误走向相反。作者用 analytics.track("view", { id: item.id }) 举例:依赖写成 [item],父组件每次渲染都创建新对象,就会反复上报。业务关心的若是 item.id,依赖也应围绕这个基本类型值表达,同时核对 effect 实际读取的内容。
依赖数组应该描述真实的数据关系,不能拿来强行控制执行次数。
useCallback 也受同样的约束。原文里的 onSave 依赖 formState,而 formState 随输入变化;另一个 effect 又依赖 onSave,负责调用 window.addEventListener 和 window.removeEventListener。结果是每次输入都重新绑定监听器。
这里 useCallback 确实按依赖工作了,只是没有提供作者想要的稳定身份。作者倾向于用 ref 读取最新值,或缩小回调依赖。但这需要先明确:监听器需要持续读取最新表单,还是应该随某些字段变化重新建立?不能只把依赖删掉。
能在渲染时算出来,就先别增加同步链
原文最容易直接采用的建议,是审查那些只负责同步已有数据的 useState 与 useEffect 组合。
例如 fullName 完全由 first、last 决定,直接写 const fullName = \${first} ${last}`;` 就够了。把它存入 state,再通过 effect 更新,会多出一个需要维持一致性的值。对字符串拼接再加 useMemo,也缺少收益依据。
作者把这种倾向归因于 AI 对 React 词汇的机械套用:需要给一个值命名,就想到 state;出现重复计算,就想到 memo。
原文还展示了三级缓存:sorted 用 [items] 排序,visible 用 [sorted, isVisible] 过滤,rows 用 [visible] 映射。只要上游每次都传入新 items,整条链仍然重新计算。
useMemo 能复用依赖未变时的结果,不能替上游修复对象身份。先检查数组从哪里创建、状态如何组织,再判断大列表和昂贵纯计算是否需要缓存,比逐层补 useMemo 更有针对性。
闭包也要按使用方式判断。原文的 useSearch 在内部定义 filter,读取 items 和 query,再立即返回 results: filter()。这个用法本身会读取当次渲染的值。风险出现在后续修改:有人把旧函数留在外部,或放进依赖为 [] 的 effect,才可能持续读取旧数据。
因此,审查重点应是「谁保存了哪个版本的函数」,而不是看到嵌套函数就认定有 stale closure。
清理要对应资源,抽象要暴露行为
异步请求最容易在演示中漏掉边界:请求尚未返回,用户已经切走页面,或者 id 已经变化。
作者给出的最低防护,是让旧请求的结果失去更新资格:
useEffect(() => {
let cancelled = false;
load(id).then((data) => {
if (!cancelled) setData(data);
});
return () => {
cancelled = true;
};
}, [id]);
要看清的是 cancelled = true:它阻止旧结果调用 setData,并没有取消 load。作者建议进一步使用 AbortController,让支持取消的请求停止;这也要求底层请求接收并使用取消信号。
同理,订阅需要 unsubscribe,setTimeout 需要对应清除,监听器需要移除。只设置标记,对避免旧结果覆盖有用,对持续消耗资源的上传循环却不够。
抽象层面,作者反对为了缩短组件而机械提取 useX。自定义 Hook 更适合承载共享行为、store 同步或多个 effect 之间的协调关系。只有一个布尔状态和 open、close 操作,是否值得单独命名,要看项目上下文。
另一条更有普适性的要求,是让副作用可见。原文的 useCartTotal 看起来只计算金额,内部却通过 localStorage.setItem 写入 "cart-total"。调用者很难从名称预测这一行为。作者建议改成 usePersistCartTotal,让持久化意图直接出现在调用处。
我的判断
这份清单适合做 AI 代码的人工审查入口,但原文没有给出团队样本、缺陷统计或性能对照。「花 5 分钟检查,省掉一下午调试」是经验性表达,不能当成验证过的收益。
我认同它对派生状态、隐藏 I/O 和无依据缓存的警惕。相比一律提取 Hook、每个函数都包 useCallback,这些检查更接近真实的数据流问题。不过,小型布尔 Hook 也可能有统一接口的价值,不能仅凭代码行数否定。
原文的 useIsOpen 示例还有直接错误:返回对象用了两个 open 键,后面的函数会覆盖前面的状态值。类型和语法层面的审查,仍然要排在抽象品味讨论之前。
还有两处不能照单全收。第一,并非每个 useEffect 都需要 cleanup,清理应对应实际建立的资源或异步生命周期。第二,Strict Mode 下开发环境的额外执行,与依赖身份变化导致的重复执行,需要分别定位。原文没有展开这一区别。
这是一份发现问题的清单,不是一套可以机械套用的改写规则。
能拿来做什么
- 审查 AI 提取的数据请求 Hook 时,先写清请求由哪个 id 驱动,再验证连续切换 id、请求乱序返回和组件卸载时的行为,区分「忽略旧结果」与「取消请求」。
- 检查表单和列表组件时,把仅由现有 props、state 算出的值优先放回渲染过程;新增 useMemo、useCallback 时,要求说明具体计算成本或身份约束。
- 给 AI 的提示词加入明确限制:仅在行为复用或 effect 关系复杂时提取 Hook,依赖必须真实,资源必须有对应清理,读取型名称不得隐藏 I/O。
- 沿用作者的合并门槛:依赖、派生状态、清理、抽象价值、缓存理由、副作用可见性这 6 项中,若有 2 项或更多不通过,就要求缩小改动,内联逻辑或重新明确 effect 的约定。