mcpField
N
no-use-effect

no-use-effect

github.com/alejandrobailo开发70734 次安装31 周前更新
代码仓库

关于 no-use-effect

面向 React 开发者的编码 Skill,建议组件避免直接调用 `useEffect`,并为派生状态、事件、数据获取、订阅和挂载时设置提供替代方案。

主要功能

  1. 01从状态和属性派生值
  2. 02将用户触发的操作放入事件处理器
  3. 03建议使用数据获取库
  4. 04使用 `useSyncExternalStore` 处理订阅
  5. 05仅在挂载时同步使用 `useMountEffect`

适用场景

  1. 01检查 React 组件中的多余副作用
  2. 02替换基于 effect 的数据获取
  3. 03处理挂载时的外部系统设置

安装

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

$npx skills add https://github.com/alejandrobailo/no-use-effect --skill no-use-effect

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

附带文件references/patterns.mdREADME.md

详细介绍

不要直接使用 useEffect

永远不要直接调用 useEffect。如果确实需要在挂载时与外部系统同步,可使用 useMountEffect()。

export function useMountEffect(effect: () => void | (() => void)) {
  /* eslint-disable react-hooks/exhaustive-deps, no-restricted-syntax */
  useEffect(effect, []);
}

只有在可复用的自定义 Hook 内部(例如 useMountEffect 本身,或在没有可用的数据获取库时使用的 useData Hook),才可以直接使用 useEffect。组件绝不能导入或调用 useEffect。

6 条规则

规则 1:派生状态,不要同步状态

如果可以根据现有状态/props 计算,就直接在行内计算。

// BAD: two render cycles
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
  setFilteredProducts(products.filter((p) => p.inStock));
}, [products]);

// GOOD: one render
const filteredProducts = products.filter((p) => p.inStock);

对于耗时的计算,可使用 useMemo:

const visibleTodos = useMemo(
  () => getFilteredTodos(todos, filter),
  [todos, filter]
);

判断标准: 你正准备编写 useEffect(() => setX(f(y)), [y]),或者创建仅用于镜像其他状态/props 的状态。

规则 2:使用数据获取库

基于 Effect 的数据获取会引发竞态条件,还会重复实现缓存功能。

// BAD: race condition
useEffect(() => {
  fetchProduct(productId).then(setProduct);
}, [productId]);

// GOOD: library handles cancellation/caching/staleness
const { data: product } = useQuery(
  ['product', productId],
  () => fetchProduct(productId)
);

在 React 19+ 中,使用 use() Hook 和 <Suspense> 来解析 Promise,无需使用 useEffect 或 useState:

// GOOD: use() + Suspense (React 19+)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

判断标准: 你的 Effect 调用了 fetch(),然后调用 setState();或者你正在重新实现缓存/重试/取消功能。

规则 3:使用事件处理程序,而不是 Effect

如果某个操作由用户操作触发,就在处理程序中执行。过程式逻辑(验证 → 转换 → 提交)应放在处理程序中。通过 ref+effect 链重建过程式流程,说明这段逻辑从一开始就是由事件驱动的。

// BAD: flag-relay through effect
const [liked, setLiked] = useState(false);
useEffect(() => {
  if (liked) { postLike(); setLiked(false); }
}, [liked]);

// GOOD: direct
<button onClick={() => postLike()}>Like</button>

要在多个处理程序之间共享逻辑,请提取一个函数,而不是使用 Effect:

function buyProduct() {
  addToCart(product);
  showNotification(`Added ${product.name}`);
}

判断标准: 将状态用作标记,让 Effect 执行实际操作;或者使用“设置标记 -> Effect 运行 -> 重置标记”这样的机制。

规则 4:一次性外部同步使用 useMountEffect

仅用于真正需要在挂载时设置外部系统的情况:DOM 集成、第三方组件、浏览器 API 订阅。

// BAD: guard inside effect
useEffect(() => {
  if (!isLoading) playVideo();
}, [isLoading]);

// GOOD: conditional mounting
function VideoPlayerWrapper({ isLoading }) {
  if (isLoading) return <LoadingScreen />;
  return <VideoPlayer />;
}
function VideoPlayer() {
  useMountEffect(() => playVideo());
}

判断标准: 某项行为自然地属于“挂载时设置,卸载时清理”,且涉及外部系统。

规则 5:使用 key 重置,不要编排依赖项

如果你需要“ID 改变时重新开始”,请使用 React 的重新挂载语义。

// BAD: effect resets on ID change
useEffect(() => { loadVideo(videoId); }, [videoId]);

// GOOD: key forces clean remount
<VideoPlayer key={videoId} videoId={videoId} />

function VideoPlayer({ videoId }) {
  useMountEffect(() => { loadVideo(videoId); });
}

这也适用于重置表单状态、清除选择项等情况。请在组件上使用 key,而不是通过 Effect 将状态设为初始值。

判断标准: Effect 唯一的作用是在 ID/prop 改变时重置本地状态。

规则 6:绝不要用 ref 修补有问题的 Effect

如果你需要用 useRef 来阻止 Effect 重复触发、陷入循环或访问过时的状态,那么问题出在 Effect 本身。请消除根本原因,而不是用 ref 打补丁。

// BAD: ref guard hides the real problem
const hasRun = useRef(false);
useEffect(() => {
  if (hasRun.current) return;
  hasRun.current = true;
  showWelcomeToast(userId);
}, [userId]);

// GOOD: useMountEffect for true one-time setup
useMountEffect(() => {
  showWelcomeToast(userId);
});
// BAD: ref to capture latest callback, dodging the dependency array
const onMessageRef = useRef(onMessage);
onMessageRef.current = onMessage;
useEffect(() => {
  const conn = createConnection(roomId);
  conn.on('message', (msg) => onMessageRef.current(msg));
  return () => conn.disconnect();
}, [roomId]);

// GOOD: useEffectEvent (experimental) captures latest values automatically
const onMsg = useEffectEvent((msg) => {
  onMessage(msg);
});
useEffect(() => {
  const conn = createConnection(roomId);
  conn.on('message', onMsg);
  return () => conn.disconnect();
}, [roomId]);

React 文档明确警告:“正确的问题不是‘如何让 Effect 只运行一次’,而是‘如何修复 Effect,使其在重新挂载后仍能正常工作’。”

判断标准: 你正在添加 hasRun.current、isMounted.current,或仅用于控制 Effect 何时/是否运行的 ref。或者,你正在 ref 中存储回调,以避免将其列为依赖项。

其他模式

通知父组件状态变化

不要使用 Effect 调用 onChange——请在事件处理程序中同时更新两者,或者将状态提升:

// BAD
useEffect(() => { onChange(isOn); }, [isOn, onChange]);

// GOOD: update both during the event
function updateToggle(nextIsOn) {
  setIsOn(nextIsOn);
  onChange(nextIsOn);
}
使用 useEffectEvent 获取最新值(实验性)

当 Effect 需要读取 prop/状态的最新值,但又不应在它变化时重新运行,可使用 useEffectEvent,而不是通过 ref 变通处理:

// BAD: ref to read latest theme without adding it as dependency
const themeRef = useRef(theme);
themeRef.current = theme;
useEffect(() => {
  logVisit(url, themeRef.current);
}, [url]);

// GOOD: useEffectEvent reads latest values automatically
const onVisit = useEffectEvent(() => {
  logVisit(url, theme);
});
useEffect(() => {
  onVisit();
}, [url]);

在 useEffectEvent 稳定之前,优先将逻辑移至事件处理程序或 useMountEffect。如果两者都不适用,请将 ref 变通处理封装在自定义 Hook 中——绝不要放在组件中。

使用回调 ref 执行 DOM 副作用

对于 DOM 测量或设置,回调 ref 比 useRef + useMountEffect 更可靠——它会在节点挂载或卸载时立即触发:

// BAD: ref.current may be null on first render
const ref = useRef(null);
useMountEffect(() => {
  setHeight(ref.current.getBoundingClientRect().height);
});
return <div ref={ref}>Content</div>;

// GOOD: callback ref fires when node is attached
const measuredRef = useCallback((node: HTMLDivElement | null) => {
  if (node !== null) {
    setHeight(node.getBoundingClientRect().height);
  }
}, []);
return <div ref={measuredRef}>Content</div>;

在 React 19+ 中,回调 ref 支持返回清理函数。对于更早的版本,请在自定义 Hook 中使用 ref 保存清理逻辑。

订阅外部存储

使用 useSyncExternalStore,而不是手动编写订阅 Effect:

const isOnline = useSyncExternalStore(
  subscribe,
  () => navigator.onLine,
  () => true
);
应用初始化

在模块级别运行一次,而不是在 Effect 中运行:

if (typeof window !== 'undefined') {
  checkAuthToken();
  loadDataFromLocalStorage();
}
计算链

绝不要将会相互触发的副作用串联起来。在行内推导值,并在事件处理函数中批量更新状态:

// BAD: 3 effects chaining state -> 3 extra renders
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]);

// GOOD: derive + handle in event
const isGameOver = round > 5;

function handlePlaceCard(nextCard) {
  setCard(nextCard);
  if (nextCard.gold) {
    if (goldCardCount < 3) setGoldCardCount(goldCardCount + 1);
    else { setGoldCardCount(0); setRound(round + 1); }
  }
}

决策清单

编写任何副作用之前,请回答以下问题:

  1. 能在渲染期间计算吗? -> 在行内推导,或使用 useMemo
  2. 由用户操作触发吗? -> 使用事件处理函数
  3. 正在获取数据吗? -> 使用数据获取库(React Query、SWR 等),或在 React 19+ 中使用 use() + Suspense
  4. 正在订阅外部存储吗? -> 使用 useSyncExternalStore
  5. 需要在 prop 变化时重置状态吗? -> 使用 key prop
  6. 确实需要在挂载时同步外部系统吗? -> 使用 useMountEffect
  7. 是否正在使用 refs 控制副作用何时或是否运行? -> 移除 refs;将逻辑移至事件处理函数中、推导得出,或使用 useEffectEvent
  8. 节点挂载时是否需要执行 DOM 副作用? -> 使用回调 ref

如果以上情况都不适用,但你仍认为需要使用 useEffect,请参阅 references/patterns.md 查看详细示例。

失败模式

  • useMountEffect 的失败是二元且明显的:要么运行了一次,要么完全没运行
  • 直接使用 useEffect 的失败会逐渐表现为不稳定行为、性能问题或循环,之后才会彻底崩溃

选择容易发现的错误。

评论

支持 Markdown