关于 use-effect-killer
用于审查 React 代码的技能,可查找误用或多余的 `useEffect` 并提出替代方案。它按反模式分类,并报告位置、示例、修复建议和统计数量。
主要功能
- 01扫描 React 源文件中的 `useEffect`
- 02识别 13 类 Effect 反模式
- 03报告文件路径和行号
- 04提供具体代码替代方案
- 05跳过明确合理的 Effect
适用场景
- 01审查 React 代码中的 Effect 用法
- 02查找通过 Effect 保存的派生状态
- 03检查数据请求是否处理过期响应
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ victor36max/ use-effect-killer --skill use-effect-killer安装后在 Claude Code 中输入/use-effect-killer确认已加载。
附带文件LICENSEREADME.md
详细介绍
useEffect 杀手
审查 React 代码库,找出不必要或误用的 useEffect 调用,并提出符合惯用写法的替代方案。
参考:https://react.dev/learn/you-might-not-need-an-effect
工作流
-
查找所有 useEffect 用法。 在
$ARGUMENTS(如果未提供路径,则扫描整个仓库)中搜索useEffect。筛选.tsx、.ts、.jsx、.js文件。 -
阅读每个文件中包含 useEffect 的内容。对每个 useEffect 调用,根据下方的反模式目录进行分类。跳过合理的副作用(例如带清理逻辑、作用域限于组件的 DOM 事件监听器、动画设置,以及与外部系统的真正同步)。
-
记录每项发现,包括:
- 文件路径和行号
- 匹配的反模式
- 有问题的代码片段
- 使用推荐替代方案的具体修复建议
-
使用底部的输出格式报告发现。 按反模式分组。附上包含数量统计的摘要。
-
如果
$ARGUMENTS为空,则扫描项目中的所有 React 组件文件。
反模式目录
使用以下模式对遇到的每个 useEffect 进行分类。
1. 派生状态
通过 useEffect 更新从其他状态或 props 计算得出的状态。
检测方法: useEffect 函数体调用了 setter,且要设置的值可以表示为渲染时已可用的状态或 props 的纯函数。
不推荐:
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
修复: 移除多余的状态和副作用,直接在代码中计算。
const fullName = firstName + ' ' + lastName;
2. 昂贵的派生计算
与 #1 相同,但由于计算开销较大,开发者使用 useEffect + state 来“缓存”结果。
检测方法: useEffect 将 props/state 中的数据转换或筛选后,存入另一个状态变量。通常会涉及 .filter()、.map()、.sort()、.reduce()。
不推荐:
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
修复: 使用 useMemo。
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
3. 在 Prop 变化时重置所有状态
使用 useEffect 在身份标识 prop(例如 userId、itemId)变化时重置状态变量。
检测方法: useEffect 函数体会将一个或多个状态变量设为初始值,且依赖数组中包含某个 prop。
不推荐:
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
修复: 提取为子组件,并使用 key 重置。
function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState('');
4. 在 Prop 变化时调整部分状态
使用 useEffect 在 props 变化时部分更新状态,而非完全重置。
检测方法: useEffect 依赖某个 prop,并有条件地更新状态——通常会清空某个选项,或重置某个子字段。
不推荐:
function List({ items }) {
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
修复(首选): 在渲染期间派生该值,而不是存储它。
function List({ items }) {
const [selectedId, setSelectedId] = useState(null);
const selection = items.find(item => item.id === selectedId) ?? null;
修复(替代方案): 存储前一个 props,并在渲染期间调整。注意:items !== prevItems 检查是必需的;没有它会导致无限渲染循环。此模式还会额外触发一次重新渲染。
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
5. 在 Effect 中处理事件逻辑
将副作用(toast、通知、导航、由用户操作触发的分析事件)放在 useEffect 中,而不是触发它的事件处理函数中。
检测方法: useEffect 调用 showNotification、toast、navigate、router.push、alert 等函数,或记录分析事件;这些调用由事件处理函数设置的状态变量触发。
不推荐:
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to cart!`);
}
}, [product]);
修复: 将逻辑移入事件处理函数。
function handleBuyClick() {
addToCart(product);
showNotification(`Added ${product.name} to cart!`);
}
6. 在 Effect 中执行 POST / 变更操作
使用状态变量作为触发器,在 useEffect 中发起网络请求。
检测方法: useEffect 调用 fetch、post、axios、mutate、useMutation、tRPC 变更操作或类似函数;其依赖项是由事件处理函数设置的状态变量(通常是“提交载荷”状态)。
不推荐:
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
修复: 直接从事件处理函数调用。
function handleSubmit(e) {
e.preventDefault();
post('/api/register', { firstName, lastName });
}
7. Effect 链
多个 useEffect 中,一个 Effect 设置状态并触发下一个 Effect,形成级联。
**检测:**同一组件中的两个或多个 useEffect 调用,其中一个的依赖项包含在另一个中设置的状态变量。留意依次设置状态的模式。
不佳:
useEffect(() => { if (card?.gold) setGoldCardCount(c => c + 1); }, [card]);
useEffect(() => { if (goldCardCount > 3) { setRound(r => r + 1); setGoldCardCount(0); } }, [goldCardCount]);
useEffect(() => { if (round > 5) setIsGameOver(true); }, [round]);
**修复:**尽可能使用派生值(const isGameOver = round > 5),其余逻辑合并到事件处理函数中。
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
}
}
}
8. 在 Effect 中初始化应用
在 useEffect([], ...) 中编写一次性设置代码(身份验证检查、读取 localStorage、加载配置),但这会导致代码在 Strict Mode 双重挂载时出现问题。
**检测:**依赖数组为 [] 的 useEffect,其中执行了初始化逻辑,例如身份验证令牌检查、读取 localStorage、全局配置加载。如果逻辑带有不应执行两次的副作用,问题尤其严重。
不佳:
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
**修复:**使用模块级标志进行保护,确保代码即使在 Strict Mode 双重挂载时也只运行一次。
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
}
对于确实没有副作用且不需要 React 生命周期的代码,也可以在模块级执行:
if (typeof window !== 'undefined') {
// Only for pure reads — not for auth tokens or anything with side effects
const cachedTheme = localStorage.getItem('theme');
}
9. 通过 Effect 通知父组件
状态更新后,在 useEffect 中调用父组件回调(如 onChange),而不是在更新状态的同时调用。
检测:useEffect 的主体调用了一个属性回调(onChange、onUpdate、onSelect 等),并传入当前状态值。
不佳:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
**修复:**在事件处理函数中调用该回调。
function handleClick() {
const nextIsOn = !isOn;
setIsOn(nextIsOn);
onChange(nextIsOn);
}
或者将组件改为完全受控组件(移除本地状态,由父组件管理状态)。
10. 通过 Effect 向父组件传递数据
子组件获取或计算数据,然后通过 Effect 回调将数据传递给父组件。
检测:useEffect 调用属性回调(如 onFetched、onData、onLoaded),并传入在子组件内部通过 Hook 或 fetch 获取的数据。
不佳:
function Child({ onFetched }) {
const data = useSomeAPI();
useEffect(() => {
if (data) onFetched(data);
}, [onFetched, data]);
**修复:**将数据获取逻辑提升到父组件中。数据应向下传递,而不是向上传递。
function Parent() {
const data = useSomeAPI();
return <Child data={data} />;
}
11. 外部 Store 订阅
在 useEffect 中手动使用 addEventListener/removeEventListener,或编写订阅逻辑,以便与浏览器 API 或外部 Store 同步。
检测:useEffect 在 window、document 或 navigator 上(而非组件 ref 上)使用 addEventListener/removeEventListener,将外部状态同步到 React 状态中。也要留意全局 Store 的 .subscribe() / .unsubscribe() 模式。不要标记带有正确清理逻辑的 ref 范围内 DOM 监听器——这些用法是合理的。
不佳:
useEffect(() => {
function update() { setIsOnline(navigator.onLine); }
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
**修复:**使用 useSyncExternalStore。
const isOnline = useSyncExternalStore(
(cb) => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
},
() => navigator.onLine,
() => true
);
12. 通过 Effect 从 Props 初始化状态
使用 useEffect 在首次渲染时根据 props 设置状态,而不是直接将 prop 传给 useState。
**检测:**依赖项为 [] 的 useEffect 调用 setter 并传入 prop 值;或者依赖项为 [prop] 的 useEffect 中,状态的初始值不同(例如 null),随后立即被覆盖。
不佳:
function Editor({ initialContent }) {
const [content, setContent] = useState(null);
useEffect(() => {
setContent(initialContent);
}, []);
}
**修复:**直接将 prop 作为初始状态值传入。
function Editor({ initialContent }) {
const [content, setContent] = useState(initialContent);
}
这样可以消除闪烁:组件不会先以 null 渲染,然后立即以 prop 值重新渲染。
13. 获取数据但未清理
在 useEffect 中获取数据,却没有处理过时响应(竞态条件)。
检测:useEffect 中包含 fetch、axios 或异步调用,并在完成时设置状态,但没有清理函数来设置 ignore 标志或调用 AbortController.abort()。
不佳:
useEffect(() => {
fetchResults(query).then(json => {
setResults(json);
});
}, [query]);
**修复:**添加 ignore 标志或中止控制器。更好的做法是使用数据获取库(TanStack Query、SWR 等)。
useEffect(() => {
let ignore = false;
fetchResults(query).then(json => {
if (!ignore) setResults(json);
});
return () => { ignore = true; };
}, [query]);
useEffect 的合理用法(不要标记)
跳过以下情况——这些都是 useEffect 的有效用法:
- 订阅限定于 ref 的 DOM 事件,并进行适当清理(例如,对特定元素 ref 使用 ResizeObserver,或对 ref 使用 IntersectionObserver)。注意:用于驱动渲染状态的 window/document 级订阅应改用
useSyncExternalStore(见 #11)。 - 运行限定于组件挂载期间的动画或计时器
- 与真正的外部系统同步(WebSocket 连接、初始化第三方小部件)
- 在组件显示时触发的分析/日志记录(而不是在用户执行操作时触发)
- 在挂载时管理焦点
输出格式
按以下结构编写报告:
## useEffect 审计结果
### 摘要
- **扫描的文件:** N
- **发现的 useEffect 实例:** N
- **检测到的反模式:** N
- **合理用法(已跳过):** N
### 发现
#### 1. 派生状态(发现 X 处)
**`src/components/UserProfile.tsx:42`**
(展示当前代码)
**问题:** fullName 是由 first 和 last 派生的,不需要使用 effect。
**修复:** 使用 `const fullName = first + ' ' + last;`,并移除 `fullName` 状态。
---
(按反模式分组,对每项发现重复以上内容)
### 无问题的文件
列出通过审查且包含 useEffect 的文件,让用户简要了解这些文件也已检查。