mcpField
N
no-unnecessary-effects

no-unnecessary-effects

github.com/cst2989开发3035 次安装27 周前更新
代码仓库

关于 no-unnecessary-effects

面向 React 开发的实践指南,帮助判断何时使用 useEffect,并为常见场景提供替代方案。重点是将效果限制在与外部系统同步的场合。

主要功能

  1. 01判断 useEffect 是否必要
  2. 02派生数据直接在渲染中计算
  3. 03将用户操作逻辑放入事件处理器
  4. 04提供外部订阅的清理建议

适用场景

  1. 01避免用 useEffect 保存派生状态
  2. 02处理点击或表单提交逻辑
  3. 03连接 WebSocket 或第三方组件
  4. 04订阅浏览器 API 或测量 DOM

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cst2989/react-tips-skill --skill no-unnecessary-effects

安装后在 Claude Code 中输入/no-unnecessary-effects确认已加载。

详细介绍

编写 useEffect 前

每次准备编写 useEffect 时,都先停下来回答这个问题:

这是否是在与外部系统同步?

外部系统:WebSocket、浏览器 API(IntersectionObserver、navigator.onLine)、第三方库(地图 SDK、图表组件)、DOM 测量、setInterval 定时器。

非外部系统:props、state、根据 props 或 state 派生的值、用户事件(点击、表单提交)。

如果答案是否定的,就不要编写 effect。使用下面的决策树找到合适的替代方案。

决策树

编写 effect 前,按顺序检查以下情况:

1. 我是在转换或派生数据吗?

直接在代码中计算。不用 state,也不用 effect。

// NEVER do this
const [filtered, setFiltered] = useState([]);
useEffect(() => {
  setFiltered(data.filter(item => item.active));
}, [data]);

// DO this
const filtered = data.filter(item => item.active);

// If genuinely expensive and not using React Compiler:
const filtered = useMemo(() => data.filter(item => item.active), [data]);
2. 我是在响应用户事件吗?

把逻辑放在事件处理函数中。Effect 响应的是渲染,而不是用户操作。

// NEVER do this
useEffect(() => {
  if (submitted) {
    performSearch(query);
    setSubmitted(false);
  }
}, [submitted, query]);

// DO this
function handleSubmit(e: FormEvent) {
  e.preventDefault();
  performSearch(query);
}
3. 我是在 prop 变化时重置 state 吗?

使用 key prop,让 React 卸载并重新挂载组件,从而获得新的 state。

// NEVER do this
useEffect(() => {
  setComment('');
}, [userId]);

// DO this — in the parent
<UserProfile key={userId} userId={userId} />
4. 我是在获取数据吗?

使用 TanStack Query(React Query)或类似的库。如果必须使用 useEffect,务必添加清理逻辑,以忽略过时的响应。

// Preferred
const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

// If useEffect is unavoidable, always handle race conditions
useEffect(() => {
  let ignore = false;
  fetchUser(userId).then(data => {
    if (!ignore) setUser(data);
  });
  return () => { ignore = true; };
}, [userId]);
5. 我是在通知父组件吗?

在事件处理函数中直接调用父组件的回调,同时调用 setState。React 会将两次更新批处理为一次渲染。

// NEVER do this
useEffect(() => {
  onChange(isOn);
}, [isOn, onChange]);

// DO this
function handleClick() {
  const next = !isOn;
  setIsOn(next);
  onChange(next);
}
6. 我是在串联多个 effect 吗?

将这一连串操作移到单个事件处理函数中。能在渲染期间派生的数据就直接派生。

// NEVER do this — three effects, three render passes
useEffect(() => { setCity(''); }, [country]);
useEffect(() => { setDistrict(''); }, [city]);
useEffect(() => { setShippingCost(calculate(country, city, district)); }, [country, city, district]);

// DO this
function handleCountryChange(newCountry: string) {
  setCountry(newCountry);
  setCity('');
  setDistrict('');
}

const shippingCost = country && city && district
  ? calculateShipping(country, city, district)
  : 0;
7. 我是在订阅外部 store 吗?

使用 useSyncExternalStore,而不是通过 useEffect 手动设置监听器。

// NEVER do this
useEffect(() => {
  const handler = () => setIsOnline(navigator.onLine);
  window.addEventListener('online', handler);
  window.addEventListener('offline', handler);
  return () => {
    window.removeEventListener('online', handler);
    window.removeEventListener('offline', handler);
  };
}, []);

// DO this
function subscribe(callback: () => void) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);

适合使用 useEffect 的情况

如果以上情况都不适用,而且对于“这是否是外部系统?”的回答确实是肯定的,那么 useEffect 就是合适的工具。例如:

  • WebSocket 连接(挂载时打开,卸载时关闭)
  • 初始化第三方组件(地图 SDK、富文本编辑器)
  • DOM 测量(绘制前使用 useLayoutEffect,绘制后使用 useEffect)
  • 订阅浏览器 API,并添加清理逻辑(IntersectionObserver、ResizeObserver)

编写有效的 effect 时:

  • 为函数命名,提高可读性:useEffect(function connectToChat() { ... })
  • 订阅或建立连接时,务必返回清理函数
  • 列出 effect 读取的所有依赖项
  • 测量 DOM 时使用 useLayoutEffect,以避免视觉闪烁

规则

  • 绝不要编写 useEffect(() => setSomething(derivedValue), [dep])——直接在代码中计算
  • 绝不要使用 useEffect 响应点击、提交或更改事件
  • 绝不要使用 useEffect 在 prop 变化时重置 state,而不先考虑 key prop
  • 绝不要串联多个 effect,让一个 effect 设置的 state 触发另一个 effect
  • 订阅外部系统时,务必添加清理函数
  • 务必为 effect 函数命名,以提高可读性

评论

支持 Markdown