mcpField
D
dont-use-use-effect

dont-use-use-effect

github.com/jonmumm开发367 次安装3 天前更新
代码仓库

关于 dont-use-use-effect

Don't Use useEffect 是面向 React 开发者的技能,指导开发者用其他方式替代不必要的 Effect。内容涵盖派生状态、事件处理、数据获取、订阅和随属性变化重置状态,也说明适合使用 Effect 的场景。

主要功能

  1. 01从属性和状态派生值
  2. 02将事件逻辑放入处理函数
  3. 03采用数据获取替代方案
  4. 04使用 useSyncExternalStore 订阅
  5. 05用 key 重置状态

适用场景

  1. 01检查组件中多余的状态 Effect
  2. 02选择 React 数据获取模式
  3. 03替换手动外部存储订阅

安装

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

$npx skills add https://github.com/jonmumm/skills --skill dont-use-use-effect

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

详细介绍

不要使用 useEffect

useEffect 是 React 中最容易被误用的 Hook 之一。在开发者使用它的绝大多数情况下,都有更简单、性能更好且更正确的替代方案。每一个不必要的 useEffect 都会引入额外的渲染周期,增加出现 bug 的风险(过时闭包、竞态条件、无限循环),并让组件更难理解。

黄金法则: Effect 用于与外部系统同步(DOM API、定时器、websocket、第三方小部件)。如果你正在做的事情是响应用户事件,或者可以根据现有状态/props 计算得出,就不需要 Effect。


1. 派生状态 — 直接计算

最常见的 useEffect 反模式:将可以根据其他状态或 props 计算得出的值存储在状态中。

// 🔴 BAD: Redundant state + unnecessary Effect
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  const [fullName, setFullName] = useState('');

  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);

  return <span>{fullName}</span>;
}

每次 firstName 或 lastName 变化时,这都会导致两次渲染:一次使用过时的 fullName,另一次则发生在 Effect 更新它之后。

// ✅ GOOD: Calculate during render
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // Derived — no state, no Effect, one render
  const fullName = firstName + ' ' + lastName;

  return <span>{fullName}</span>;
}

对于开销较大的计算,请使用 useMemo 包装,而不是将结果存储在状态中:

// ✅ GOOD: Memoize expensive derivations
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // Only recomputes when todos or filter change, not when newTodo changes
  const visibleTodos = useMemo(
    () => getFilteredTodos(todos, filter),
    [todos, filter]
  );

  return <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}

2. 事件驱动逻辑 — 使用事件处理函数

如果代码应该因为用户做了某件事(点击、提交、输入)而运行,就应该放在事件处理函数中,而不是 Effect 中。Effect 是因为组件进行了渲染而运行,而不是因为用户执行了某项操作。

// 🔴 BAD: Event-specific logic triggered by state change in an Effect
function ProductPage({ product, addToCart }) {
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }
  // ...
}

这会导致 bug:如果页面重新加载时商品已经在购物车中,就会触发通知。

// ✅ GOOD: Side effects belong in the event handler that caused them
function ProductPage({ product, addToCart }) {
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}

经验法则: 如果你能指出应该触发这段代码的确切用户交互,就把代码放在该交互对应的事件处理函数中。


3. Effect 链 — 合并到事件处理函数中

将多个根据其他状态设置状态的 Effect 串联起来,会造成不必要的重复渲染,并使代码变得僵化、脆弱。

// 🔴 BAD: Chain of Effects triggering each other (4 render passes!)
function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  useEffect(() => {
    if (card !== null && 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 what you can, compute the rest in the event handler
function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // Derived — no state needed
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) throw Error('Game already ended.');

    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount < 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) alert('Good game!');
      }
    }
  }
  // ...
}

4. 数据获取 — 使用数据获取库

在 useEffect 中获取数据很脆弱。你必须手动处理竞态条件、加载状态、缓存、错误处理和清理。直接使用 useEffect 获取数据没有缓存、没有去重,也不支持 SSR。

// 🔴 BAD: Manual fetch in useEffect — race conditions, no caching, no SSR
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    let ignore = false;
    setIsLoading(true);
    fetchResults(query).then(json => {
      if (!ignore) {
        setResults(json);
        setIsLoading(false);
      }
    });
    return () => { ignore = true; };
  }, [query]);
  // ...
}
// ✅ GOOD: Use TanStack Query (or your framework's data primitive)
import { useQuery } from '@tanstack/react-query';

function SearchResults({ query }) {
  const { data: results = [], isLoading } = useQuery({
    queryKey: ['search', query],
    queryFn: () => fetchResults(query),
    enabled: !!query,
  });
  // Automatic caching, deduplication, race-condition handling,
  // background refetching, and SSR support — for free.
}

其他不错的替代方案:

  • Next.js / Remix: loader 函数、Server Components、use() + Suspense
  • SWR: useSWR(key, fetcher) — 与 TanStack Query 类似
  • tRPC: 端到端类型安全的数据获取,并内置 React Query 集成

如果你必须使用 useEffect 获取数据(没有框架或库),至少要:

  1. 添加 ignore 清理标记,以防止竞态条件
  2. 将逻辑提取为可复用的自定义 Hook(例如 useData(url))
  3. 明确处理加载、错误和空状态

5. 外部存储订阅 — 使用 useSyncExternalStore

使用 useEffect + setState 订阅外部数据源(浏览器 API、第三方存储、可观察对象),不仅需要编写大量样板代码,还可能在并发模式下导致 tearing。

// 🔴 BAD: Manual subscription with useEffect
function OnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <span>{isOnline ? '✅ Online' : '❌ Offline'}</span>;
}
// ✅ GOOD: useSyncExternalStore — concurrent-safe, less code
import { useSyncExternalStore } from 'react';

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

function getSnapshot() {
  return navigator.onLine;
}

function OnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return <span>{isOnline ? '✅ Online' : '❌ Offline'}</span>;
}

对于状态管理库,请使用其内置的 React Hook,而不是手动订阅。请参阅 react-render-performance 技能,了解使用 XState、Zustand、Redux 和 Nanostores 的基于选择器的模式。


6. 属性变化时重置状态 — 使用 key

不要使用 useEffect 在 prop 变化时重置状态。React 内置了一种机制:key prop。

// 🔴 BAD: Resetting state in an Effect
function EditProfile({ userId }) {
  const [comment, setComment] = useState('');

  useEffect(() => {
    setComment('');
  }, [userId]);

  return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
// ✅ GOOD: Use key to reset component state
function ProfilePage({ userId }) {
  // When userId changes, React unmounts the old EditProfile and mounts a new one
  return <EditProfile userId={userId} key={userId} />;
}

function EditProfile({ userId }) {
  const [comment, setComment] = useState('');
  return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}

useEffect 适用的情况

当需要将组件与React 之外的事物同步时,使用 Effect 是合适的:

使用场景为什么合适
集成非 React 组件(地图、图表、视频播放器)将 React 状态同步到命令式 DOM API
建立 WebSocket 或 EventSource 连接外部系统的生命周期与组件绑定
测量 DOM 布局(getBoundingClientRect、ResizeObserver)读取渲染后的 DOM 信息
以命令式方式管理焦点或滚动位置命令式 DOM 交互
连接硬件 API(摄像头、地理位置)具有订阅和清理操作的外部系统
document.title 或其他全局副作用将 React 状态同步到浏览器 API

即使在这些情况下,也要考虑是否已有库可以处理这些工作(例如,react-intersection-observer、framer-motion)。


决策流程图

编写 useEffect 之前,请先问自己以下问题:

  1. 能否根据现有 props/state 计算出这个值? → 在渲染期间推导(或使用 useMemo)。
  2. 这是对用户操作的响应吗? → 将逻辑放在事件处理函数中。
  3. 我在获取数据吗? → 使用 TanStack Query、SWR 或框架的数据原语。
  4. 我在订阅外部存储吗? → 使用 useSyncExternalStore 或库提供的 Hook。
  5. 我在 prop 发生变化时重置状态吗? → 使用 key prop。
  6. 我在与 React 无法控制的外部系统同步吗? → ✅ 这是有效的 useEffect 用法。

总结检查清单

  • 不要使用 useEffect 来设置从其他状态/props 推导出的状态——应改为在渲染期间计算
  • 不要使用 useEffect 来响应用户事件执行逻辑——应改为使用事件处理函数
  • 不要让多个 useEffect 通过状态更新相互触发形成调用链——应改为合并到事件处理函数中,或使用推导值
  • 不要使用原始 useEffect 获取数据——应改为使用 TanStack Query、SWR 或框架的数据原语
  • 不要使用 useEffect + setState 订阅外部存储——应改为使用 useSyncExternalStore 或库提供的 Hook
  • 不要在 props 发生变化时使用 useEffect 重置状态——应改为使用 key prop
  • 所有保留的 useEffect 都应与真正的外部系统(DOM API、WebSocket、第三方组件)同步

评论

支持 Markdown