No Use Effect

作者 alejandrobailo1f77fbcbbb0d71b9479721d57a5db04d1dc95713无许可证67 个星标收录于 2026年10月9日更新于 2026年10月9日仓库6个月前更新

ALWAYS ACTIVE. Enforces a strict no-direct-useEffect rule in React and React Native code. Never write useEffect directly — use derived state, event handlers, useMemo, useSyncExternalStore, data-fetching libraries, key-based resets, or useMountEffect instead. Triggers on all React component code: writing new components, editing existing ones, reviewing code, fixing bugs, or refactoring. Also triggers when user mentions useEffect, side effects, data fetching, state sync, or component lifecycle.

AI 生成的概览

在 React 与 React Native 代码中强制禁止直接使用 useEffect,并给出替代方案。

功能
该技能提供一套编码规范,禁止在 React 或 React Native 组件中直接调用 useEffect。它列出六条规则,配有反例与正例代码,涵盖派生状态、数据获取库、事件处理函数、useMountEffect 辅助函数、基于 key 的重置,以及避免用 ref 打补丁。此外还包含补充模式、决策清单和失败模式说明,并附有参考文件提供更多示例。
适用场景
适用于编写、修改、审查或重构 React 与 React Native 组件时,尤其是涉及副作用、数据获取、状态同步或组件生命周期的场景。其目的是引导代码避免直接使用 useEffect,改用所列替代方案。
运行要求
无需脚本或软件包,仅为说明性指令。该指南假定存在 React 或 React Native 代码库,并提及可选库(如 React Query、SWR)以及 React 19+ 的 use() 和 useEffectEvent 等特性。

No Direct useEffect

Never call useEffect directly. For the rare case of syncing with an external system on mount, use useMountEffect().

tsx
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.

The 6 Rules

Rule 1: Derive state, do not sync it

If you can calculate it from existing state/props, compute it inline.

tsx
// BAD: two render cyclesconst [filteredProducts, setFilteredProducts] = useState([]);useEffect(() => {  setFilteredProducts(products.filter((p) => p.inStock));}, [products]);
// GOOD: one renderconst filteredProducts = products.filter((p) => p.inStock);

For expensive calculations, use useMemo:

tsx
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.

Rule 2: Use data-fetching libraries

Effect-based fetching creates race conditions and reinvents caching.

tsx
// BAD: race conditionuseEffect(() => {  fetchProduct(productId).then(setProduct);}, [productId]);
// GOOD: library handles cancellation/caching/stalenessconst { data: product } = useQuery(  ['product', productId],  () => fetchProduct(productId));

In React 19+, use the use() hook with <Suspense> to unwrap promises without useEffect or useState:

tsx
// 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.

Rule 3: Event handlers, not effects

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.

tsx
// BAD: flag-relay through effectconst [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:

tsx
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.

Rule 4: useMountEffect for one-time external sync

Only for true mount-time external system setup: DOM integration, third-party widgets, browser API subscriptions.

tsx
// BAD: guard inside effectuseEffect(() => {  if (!isLoading) playVideo();}, [isLoading]);
// GOOD: conditional mountingfunction 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.

Rule 5: Reset with key, not dependency choreography

If you need "start fresh when ID changes," use React's remount semantics.

tsx
// BAD: effect resets on ID changeuseEffect(() => { 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.

Rule 6: Never patch a broken effect with a ref

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.

tsx
// BAD: ref guard hides the real problemconst hasRun = useRef(false);useEffect(() => {  if (hasRun.current) return;  hasRun.current = true;  showWelcomeToast(userId);}, [userId]);
// GOOD: useMountEffect for true one-time setupuseMountEffect(() => {  showWelcomeToast(userId);});
tsx
// BAD: ref to capture latest callback, dodging the dependency arrayconst 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 automaticallyconst 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.

Additional Patterns

Notifying parents about state changes

Do not use an effect to call onChange — update both in the event handler, or lift state up:

tsx
// BADuseEffect(() => { onChange(isOn); }, [isOn, onChange]);
// GOOD: update both during the eventfunction updateToggle(nextIsOn) {  setIsOn(nextIsOn);  onChange(nextIsOn);}

useEffectEvent for latest values (experimental)

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:

tsx
// BAD: ref to read latest theme without adding it as dependencyconst themeRef = useRef(theme);themeRef.current = theme;useEffect(() => {  logVisit(url, themeRef.current);}, [url]);
// GOOD: useEffectEvent reads latest values automaticallyconst 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.

Callback refs for DOM side effects

For DOM measurement or setup, a callback ref is more reliable than useRef + useMountEffect — it fires exactly when the node attaches or detaches:

tsx
// BAD: ref.current may be null on first renderconst ref = useRef(null);useMountEffect(() => {  setHeight(ref.current.getBoundingClientRect().height);});return <div ref={ref}>Content</div>;
// GOOD: callback ref fires when node is attachedconst 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.

Subscribing to external stores

Use useSyncExternalStore instead of manual subscription effects:

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

App initialization

Run once at module level, not in an effect:

tsx
if (typeof window !== 'undefined') {  checkAuthToken();  loadDataFromLocalStorage();}

Chains of computations

Never chain effects that trigger each other. Derive values inline and batch state updates in the event handler:

tsx
// BAD: 3 effects chaining state -> 3 extra rendersuseEffect(() => { 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 eventconst isGameOver = round > 5;
function handlePlaceCard(nextCard) {  setCard(nextCard);  if (nextCard.gold) {    if (goldCardCount < 3) setGoldCardCount(goldCardCount + 1);    else { setGoldCardCount(0); setRound(round + 1); }  }}

Decision Checklist

Before writing any effect, answer these questions:

  1. Can I compute it during render? -> Derive it inline or useMemo
  2. Is it triggered by a user action? -> Event handler
  3. Am I fetching data? -> Data-fetching library (React Query, SWR, etc.) or use() + Suspense in React 19+
  4. Am I subscribing to an external store? -> useSyncExternalStore
  5. Do I need to reset state when a prop changes? -> key prop
  6. Is it true mount-time external system sync? -> useMountEffect
  7. Am I using refs to control when/if an effect runs? -> Remove the refs; move logic to an event handler, derive it, or use useEffectEvent
  8. Do I need a DOM side effect when a node mounts? -> Callback ref

If none of the above apply and you still think you need useEffect, see references/patterns.md [blocked] for detailed examples.

Failure Modes

  • useMountEffect failures are binary and loud: it ran once, or not at all
  • Direct useEffect failures degrade gradually as flaky behavior, perf issues, or loops before a hard crash

Choose the bug that's easy to find.

来源与署名

来源:alejandrobailo/no-use-effect提交1f77fbc

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架