Rule — React 前端 lifecycle 清单¶
类比 MV3持久化陷阱清单:那个管 background;这个管前端。
为什么需要¶
v0.10.45 第二轮独立 agent 元-评估发现:本会话 14+ issue 0 个针对前端 lifecycle。 334 处 useEffect/useState 中除了用 useRequest 的,其余裸奔(无 cleanup)。
具体踩坑: - v0.10.45 ISSUE-0029 S4:local-data-view onMessage 无 cleanup - v0.10.46 ISSUE-0030:use-countdown 双 useEffect 反模式
这两个都是同一类问题:useEffect 注册副作用但忘了 cleanup。
自查清单¶
1️⃣ useEffect 注册的副作用必须 cleanup¶
| 副作用 | 必须 cleanup |
|---|---|
setInterval / setTimeout(长延迟) |
✅ clearInterval/Timeout |
addEventListener |
✅ removeEventListener |
chrome.runtime.onMessage.addListener / webext-bridge onMessage |
✅ 返回 off() |
new EventSource / new WebSocket |
✅ .close() |
new IntersectionObserver / MutationObserver / ResizeObserver |
✅ .disconnect() |
仅 setState(同步 / async) |
🟢 不必(但要看 race) |
useRequest(ahooks) |
🟢 自带 unmount cancel |
2️⃣ 标准 pattern¶
// ✅ 正确
useEffect(() => {
const timer = setInterval(() => { ... }, 1000);
const off = onMessage('xxx', handler);
window.addEventListener('resize', onResize);
return () => {
clearInterval(timer);
off?.();
window.removeEventListener('resize', onResize);
};
}, []);
// ❌ 反模式 1:忘 cleanup
useEffect(() => {
setInterval(...);
}, []);
// ❌ 反模式 2:拆两个 useEffect 用 mod-let 传递(v0.10.46 修过这个)
let timer;
useEffect(() => { timer = setInterval(...); }, []);
useEffect(() => () => clearInterval(timer), [timer]);
3️⃣ async fetch + setState 的 unmount race¶
// ❌ 反模式:unmount 后 setState warning
useEffect(() => {
fetch(url).then(d => setData(d)); // 组件 unmount 后还可能 setData
}, []);
// ✅ 用 useRequest(推荐)
const { data } = useRequest(() => fetch(url));
// ✅ 或手动 mounted ref
useEffect(() => {
let alive = true;
fetch(url).then(d => { if (alive) setData(d); });
return () => { alive = false; };
}, []);
4️⃣ AbortController for 真的能取消的 fetch¶
useEffect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal }).then(...);
return () => ctrl.abort();
}, []);
自查命令¶
# v0.10.46 起一键
pnpm scan:react
# 保存基线
pnpm scan:react -- --save-baseline
# 只显示新增(pre-commit 用)
pnpm scan:react -- --diff
脚本:scripts/scan-react-lifecycle.py。扫所有 useEffect 块内含副作用 keyword
(addListener / setInterval / new EventSource 等)但无 return cleanup 的位置。
判定指南(写在脚本输出里): - 🔴 addListener / onMessage / EventSource → 几乎必修 - 🟡 setInterval / setTimeout → 看是否别处清理 - 🟢 仅 setState → 不必
触发场景¶
| 场景 | 必跑 / 可选 |
|---|---|
| 写完 React 组件 + 含 useEffect | 必跑 |
| review 1-2 版本前的前端代码 | 必跑 |
| debug "扩展用久了变卡 / 数据不更新" | 必跑 |
| 跨平台 / 浏览器特定 bug | 可选 |
工作流¶
写新组件 / 改老组件
↓
含 useEffect 副作用?
↓ 是
确认 return cleanup
↓
git commit
↓
pre-commit hook 自动跑 scan:react --diff
↓
0 新增 → 通过
有新增 → 阻止 commit 或要求 --save-baseline 重存
与 useRequest 的关系¶
ahooks 的 useRequest 已经处理了:
- unmount 自动 cancel
- 轮询的 cleanup
- error / loading 状态管理
优先用 useRequest,自己写裸 useEffect+async 是反模式。
反模式速查¶
❌ "我用了 [] deps 所以不需要 cleanup" → 错。cleanup 是 unmount 时跑,与 deps 无关
❌ "setInterval 1s 短的没事" → 错。组件 unmount 后 setInterval 还在跑 = 内存泄漏 + 后台逻辑乱
❌ "我手动 mounted ref 就够了" → 部分对。但 addListener 仍需 cleanup(不止 setState 问题)
❌ "我用两个 useEffect,一个注册一个 cleanup" → 反模式(v0.10.46 ISSUE-0030 案例)
历史案例¶
- [[0029-dataview-perf-listener-no-cleanup|0029-DataView性能浪费索引-listener无清理]] — onMessage 无 cleanup
- 待加:v0.10.46 ISSUE-0030 use-countdown 双 useEffect 反模式
元规则¶
技术规则(如本文)能扫到的:useEffect cleanup 缺失。 技术规则扫不到的:业务逻辑层面 unmount race(如组件依赖 prop 变化)。 后者需要独立agent审查。