关于 no-use-effect
面向 React 开发者的编码 Skill,建议组件避免直接调用 `useEffect`,并为派生状态、事件、数据获取、订阅和挂载时设置提供替代方案。
主要功能
- 01从状态和属性派生值
- 02将用户触发的操作放入事件处理器
- 03建议使用数据获取库
- 04使用 `useSyncExternalStore` 处理订阅
- 05仅在挂载时同步使用 `useMountEffect`
适用场景
- 01检查 React 组件中的多余副作用
- 02替换基于 effect 的数据获取
- 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); }
}
}
决策清单
编写任何副作用之前,请回答以下问题:
- 能在渲染期间计算吗? -> 在行内推导,或使用
useMemo - 由用户操作触发吗? -> 使用事件处理函数
- 正在获取数据吗? -> 使用数据获取库(React Query、SWR 等),或在 React 19+ 中使用
use()+ Suspense - 正在订阅外部存储吗? -> 使用
useSyncExternalStore - 需要在 prop 变化时重置状态吗? -> 使用
keyprop - 确实需要在挂载时同步外部系统吗? -> 使用
useMountEffect - 是否正在使用 refs 控制副作用何时或是否运行? -> 移除 refs;将逻辑移至事件处理函数中、推导得出,或使用
useEffectEvent - 节点挂载时是否需要执行 DOM 副作用? -> 使用回调 ref
如果以上情况都不适用,但你仍认为需要使用 useEffect,请参阅 references/patterns.md 查看详细示例。
失败模式
- useMountEffect 的失败是二元且明显的:要么运行了一次,要么完全没运行
- 直接使用 useEffect 的失败会逐渐表现为不稳定行为、性能问题或循环,之后才会彻底崩溃
选择容易发现的错误。