mcpField
U
use-effect-killer

use-effect-killer

github.com/victor36max测试与质量1070 次安装29 周前更新
代码仓库

关于 use-effect-killer

用于审查 React 代码的技能,可查找误用或多余的 `useEffect` 并提出替代方案。它按反模式分类,并报告位置、示例、修复建议和统计数量。

主要功能

  1. 01扫描 React 源文件中的 `useEffect`
  2. 02识别 13 类 Effect 反模式
  3. 03报告文件路径和行号
  4. 04提供具体代码替代方案
  5. 05跳过明确合理的 Effect

适用场景

  1. 01审查 React 代码中的 Effect 用法
  2. 02查找通过 Effect 保存的派生状态
  3. 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

工作流

  1. 查找所有 useEffect 用法。 在 $ARGUMENTS(如果未提供路径,则扫描整个仓库)中搜索 useEffect。筛选 .tsx、.ts、.jsx、.js 文件。

  2. 阅读每个文件中包含 useEffect 的内容。对每个 useEffect 调用,根据下方的反模式目录进行分类。跳过合理的副作用(例如带清理逻辑、作用域限于组件的 DOM 事件监听器、动画设置,以及与外部系统的真正同步)。

  3. 记录每项发现,包括:

    • 文件路径和行号
    • 匹配的反模式
    • 有问题的代码片段
    • 使用推荐替代方案的具体修复建议
  4. 使用底部的输出格式报告发现。 按反模式分组。附上包含数量统计的摘要。

  5. 如果 $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 的文件,让用户简要了解这些文件也已检查。

评论

支持 Markdown