no-use-effect by alejandrobailo/no-use-effect
npx skills add https://github.com/alejandrobailo/no-use-effect --skill no-use-effect切勿直接调用 useEffect。对于极少数需要在挂载时与外部系统同步的情况,请使用 useMountEffect()。
export function useMountEffect(effect: () => void | (() => void)) {
/* eslint-disable react-hooks/exhaustive-deps, no-restricted-syntax */
useEffect(effect, []);
}
useEffect 唯一可以直接出现的地方是在可复用的自定义钩子内部(例如 useMountEffect 本身,或者在无法使用数据获取库时的 useData 钩子)。组件绝不能导入或调用 useEffect。
如果可以从现有状态或属性计算得出,请内联计算。
// 错误:两个渲染周期
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(products.filter((p) => p.inStock));
}, [products]);
// 正确:一个渲染周期
const filteredProducts = products.filter((p) => p.inStock);
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
对于开销较大的计算,使用 useMemo:
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
异味检测: 你正准备编写 useEffect(() => setX(f(y)), [y]) 或声明一个仅镜像其他状态/属性的状态。
基于 Effect 的数据获取会产生竞态条件并重复实现缓存。
// 错误:竞态条件
useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
// 正确:库处理取消/缓存/过期
const { data: product } = useQuery(
['product', productId],
() => fetchProduct(productId)
);
在 React 19+ 中,使用 use() 钩子配合 <Suspense> 来解开 Promise,无需 useEffect 或 useState:
// 正确:use() + Suspense (React 19+)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
异味检测: 你的 Effect 执行 fetch() 然后 setState(),或者你正在重新实现缓存/重试/取消功能。
如果由用户操作触发,请在处理器中完成工作。过程性逻辑(验证 → 转换 → 提交)属于处理器。通过 ref+Effect 链重构过程性流程,表明该逻辑从一开始就是事件驱动的。
// 错误:通过 Effect 传递标志
const [liked, setLiked] = useState(false);
useEffect(() => {
if (liked) { postLike(); setLiked(false); }
}, [liked]);
// 正确:直接处理
<button onClick={() => postLike()}>点赞</button>
对于处理器之间的共享逻辑,提取一个函数——而不是一个 Effect:
function buyProduct() {
addToCart(product);
showNotification(`已添加 ${product.name}`);
}
异味检测: 将状态用作标志,以便 Effect 执行实际动作,或者出现"设置标志 -> Effect 运行 -> 重置标志"的机制。
仅用于真正的挂载时外部系统设置:DOM 集成、第三方小部件、浏览器 API 订阅。
// 错误:在 Effect 内部进行守卫
useEffect(() => {
if (!isLoading) playVideo();
}, [isLoading]);
// 正确:条件性挂载
function VideoPlayerWrapper({ isLoading }) {
if (isLoading) return <LoadingScreen />;
return <VideoPlayer />;
}
function VideoPlayer() {
useMountEffect(() => playVideo());
}
异味检测: 行为本质上是"挂载时设置,卸载时清理"并与外部系统交互。
如果需要"当 ID 改变时重新开始",请使用 React 的重新挂载语义。
// 错误:Effect 在 ID 改变时重置
useEffect(() => { loadVideo(videoId); }, [videoId]);
// 正确:key 强制干净的重新挂载
<VideoPlayer key={videoId} videoId={videoId} />
function VideoPlayer({ videoId }) {
useMountEffect(() => { loadVideo(videoId); });
}
这也适用于重置表单状态、清除选择等。在组件上使用 key,而不是使用一个将状态设置为初始值的 Effect。
异味检测: Effect 的唯一工作是在 ID/属性改变时重置本地状态。
如果需要 useRef 来阻止 Effect 重复触发、循环或访问过时状态,那么问题在于 Effect 本身。消除根源的 Effect——不要用 ref 来打补丁。
// 错误:ref 守卫掩盖了真正的问题
const hasRun = useRef(false);
useEffect(() => {
if (hasRun.current) return;
hasRun.current = true;
showWelcomeToast(userId);
}, [userId]);
// 正确:对于真正的一次性设置,使用 useMountEffect
useMountEffect(() => {
showWelcomeToast(userId);
});
// 错误:使用 ref 捕获最新的回调,规避依赖数组
const onMessageRef = useRef(onMessage);
onMessageRef.current = onMessage;
useEffect(() => {
const conn = createConnection(roomId);
conn.on('message', (msg) => onMessageRef.current(msg));
return () => conn.disconnect();
}, [roomId]);
// 正确:useEffectEvent (实验性) 自动捕获最新值
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——在事件处理器中同时更新,或者将状态提升:
// 错误
useEffect(() => { onChange(isOn); }, [isOn, onChange]);
// 正确:在事件期间同时更新
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn);
}
当 Effect 需要读取属性/状态的最新值,但又不想在该值改变时重新运行,请使用 useEffectEvent 代替 ref 的变通方法:
// 错误:使用 ref 读取最新的 theme 而不将其添加为依赖项
const themeRef = useRef(theme);
themeRef.current = theme;
useEffect(() => {
logVisit(url, themeRef.current);
}, [url]);
// 正确:useEffectEvent 自动读取最新值
const onVisit = useEffectEvent(() => {
logVisit(url, theme);
});
useEffect(() => {
onVisit();
}, [url]);
在 useEffectEvent 稳定之前,优先将逻辑移至事件处理器或 useMountEffect。如果两者都不合适,将 ref 变通方法隔离在自定义钩子中——绝不要在组件中。
对于 DOM 测量或设置,回调 ref 比 useRef + useMountEffect 更可靠——它会在节点附加或分离时精确触发:
// 错误:ref.current 在首次渲染时可能为 null
const ref = useRef(null);
useMountEffect(() => {
setHeight(ref.current.getBoundingClientRect().height);
});
return <div ref={ref}>内容</div>;
// 正确:回调 ref 在节点附加时触发
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return <div ref={measuredRef}>内容</div>;
在 React 19+ 中,回调 ref 支持清理返回值。对于早期版本,将清理逻辑存储在自定义钩子内部的 ref 中。
使用 useSyncExternalStore 代替手动订阅的 Effect:
const isOnline = useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true
);
在模块级别运行一次,而不是在 Effect 中:
if (typeof window !== 'undefined') {
checkAuthToken();
loadDataFromLocalStorage();
}
切勿链接相互触发的 Effect。内联派生值,并在事件处理器中批量更新状态:
// 错误:3 个 Effect 链式触发状态变化 -> 3 次额外渲染
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;
function handlePlaceCard(nextCard) {
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) setGoldCardCount(goldCardCount + 1);
else { setGoldCardCount(0); setRound(round + 1); }
}
}
在编写任何 Effect 之前,回答以下问题:
useMemouse() + SuspenseuseSyncExternalStorekey 属性useMountEffectuseEffectEvent如果以上都不适用,你仍然认为需要 useEffect,请参阅 references/patterns.md 获取详细示例。
选择那个容易发现的错误。
每周安装量
77
代码仓库
GitHub 星标数
38
首次出现
8 天前
安全审计
安装于
kimi-cli77
gemini-cli77
amp77
cline77
github-copilot77
codex77
Never call useEffect directly. For the rare case of syncing with an external system on mount, use useMountEffect().
export function useMountEffect(effect: () => void | (() => void)) {
/* eslint-disable react-hooks/exhaustive-deps, no-restricted-syntax */
useEffect(effect, []);
}
The only place useEffect may appear directly is inside reusable custom hooks (like useMountEffect itself, or a useData hook when no fetching library is available). Components must never import or call useEffect.
If you can calculate it from existing state/props, compute it inline.
// 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);
For expensive calculations, use useMemo:
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
Smell test: You're about to write useEffect(() => setX(f(y)), [y]) or state that only mirrors other state/props.
Effect-based fetching creates race conditions and reinvents caching.
// BAD: race condition
useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
// GOOD: library handles cancellation/caching/staleness
const { data: product } = useQuery(
['product', productId],
() => fetchProduct(productId)
);
In React 19+, use the use() hook with <Suspense> to unwrap promises without useEffect or useState:
// GOOD: use() + Suspense (React 19+)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
Smell test: Your effect does fetch() then setState(), or you're reimplementing caching/retries/cancellation.
If a user action triggers it, do the work in the handler. Procedural logic (validate → transform → submit) belongs in handlers. Reconstructing procedural flow through ref+effect chains is a sign the logic was event-driven from the start.
// BAD: flag-relay through effect
const [liked, setLiked] = useState(false);
useEffect(() => {
if (liked) { postLike(); setLiked(false); }
}, [liked]);
// GOOD: direct
<button onClick={() => postLike()}>Like</button>
For shared logic between handlers, extract a function — not an effect:
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name}`);
}
Smell test: State used as a flag so an effect can do the real action, or "set flag -> effect runs -> reset flag" mechanics.
Only for true mount-time external system setup: DOM integration, third-party widgets, browser API subscriptions.
// 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());
}
Smell test: Behavior is naturally "setup on mount, cleanup on unmount" with an external system.
If you need "start fresh when ID changes," use React's remount semantics.
// 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); });
}
This also applies to resetting form state, clearing selections, etc. Use key on the component instead of an effect that sets state to initial values.
Smell test: Effect's only job is to reset local state when an ID/prop changes.
If you need a useRef to stop an effect from double-firing, looping, or accessing stale state, the effect itself is the problem. Eliminate the root effect — don't bandage it with a 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]);
The React docs explicitly warn: "The right question isn't 'how to run an Effect once', but 'how to fix my Effect so that it works after remounting'."
Smell test: You're adding hasRun.current, isMounted.current, or a ref whose sole purpose is controlling when/if an effect runs. Or you're storing a callback in a ref to avoid listing it as a dependency.
Do not use an effect to call onChange — update both in the event handler, or lift state up:
// BAD
useEffect(() => { onChange(isOn); }, [isOn, onChange]);
// GOOD: update both during the event
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn);
}
When an effect needs to read the latest value of a prop/state without re-running when it changes, use useEffectEvent instead of a ref workaround:
// 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]);
Until useEffectEvent is stable, prefer moving logic to event handlers or useMountEffect. If neither fits, isolate the ref workaround in a custom hook — never in a component.
For DOM measurement or setup, a callback ref is more reliable than useRef + useMountEffect — it fires exactly when the node attaches or detaches:
// 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>;
In React 19+, callback refs support cleanup return values. For earlier versions, store cleanup logic in a ref inside a custom hook.
Use useSyncExternalStore instead of manual subscription effects:
const isOnline = useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true
);
Run once at module level, not in an effect:
if (typeof window !== 'undefined') {
checkAuthToken();
loadDataFromLocalStorage();
}
Never chain effects that trigger each other. Derive values inline and batch state updates in the event handler:
// 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); }
}
}
Before writing any effect, answer these questions:
useMemouse() + Suspense in React 19+useSyncExternalStorekey propuseMountEffectuseEffectEventIf none of the above apply and you still think you need useEffect, see references/patterns.md for detailed examples.
Choose the bug that's easy to find.
Weekly Installs
77
Repository
GitHub Stars
38
First Seen
8 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
kimi-cli77
gemini-cli77
amp77
cline77
github-copilot77
codex77
UI组件模式实战指南:构建可复用React组件库与设计系统
10,700 周安装