dont-use-use-effect

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Don't Use useEffect

不要使用useEffect

useEffect
is one of the most misused hooks in React. In the vast majority of cases where developers reach for it, there is a simpler, more performant, and more correct alternative. Every unnecessary
useEffect
introduces an extra render cycle, increases the risk of bugs (stale closures, race conditions, infinite loops), and makes components harder to reason about.
The golden rule: Effects are for synchronizing with external systems (DOM APIs, timers, websockets, third-party widgets). If the work you're doing is a response to a user event, or can be calculated from existing state/props, you don't need an Effect.

useEffect
是React中最常被误用的Hook之一。在开发者想要使用它的绝大多数场景中,都存在更简单、性能更优且更正确的替代方案。每一个不必要的
useEffect
都会引入额外的渲染周期,增加出现bug的风险(如过时闭包、竞态条件、无限循环),并让组件更难理解。
黄金准则: Effects仅用于与外部系统同步(DOM API、定时器、WebSocket、第三方组件)。如果你要处理的是用户事件的响应,或者可以通过现有state/props计算得到的内容,那么你不需要Effect。

1. Derived State — Just Calculate It

1. 派生状态——直接计算即可

The most common
useEffect
anti-pattern: storing a value in state that can be computed from other state or props.
tsx
// 🔴 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>;
}
This causes two renders every time
firstName
or
lastName
changes: one with the stale
fullName
, and another after the Effect updates it.
tsx
// ✅ 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>;
}
For expensive calculations, wrap in
useMemo
instead of storing in state:
tsx
// ✅ 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>;
}

最常见的
useEffect
反模式:将可以通过其他state或props计算得到的值存储为状态。
tsx
// 🔴 错误:冗余状态 + 不必要的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更新它之后。
tsx
// ✅ 正确:在渲染时直接计算
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 派生值——无需状态,无需Effect,仅一次渲染
  const fullName = firstName + ' ' + lastName;

  return <span>{fullName}</span>;
}
对于计算成本较高的场景,使用
useMemo
包裹而非存储为状态:
tsx
// ✅ 正确:对开销大的派生值进行记忆化
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // 仅在todos或filter变化时重新计算,newTodo变化时不会触发
  const visibleTodos = useMemo(
    () => getFilteredTodos(todos, filter),
    [todos, filter]
  );

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

2. Event-Driven Logic — Use Event Handlers

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

If code should run because the user did something (clicked, submitted, typed), it belongs in an event handler — not an Effect. Effects run because a component rendered, not because the user took an action.
tsx
// 🔴 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);
  }
  // ...
}
This is buggy: the notification fires on page reload if the product is already in the cart.
tsx
// ✅ 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');
  }
  // ...
}
Rule of thumb: If you can point to the exact user interaction that should trigger the code, put it in that interaction's event handler.

如果代码是响应用户操作(点击、提交、输入)而执行的,那么它应该放在事件处理函数中——而不是Effect里。Effects是因为组件完成渲染而执行,而非用户触发了某个操作。
tsx
// 🔴 错误:通过Effect监听state变化触发事件专属逻辑
function ProductPage({ product, addToCart }) {
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }
  // ...
}
这种写法存在bug:如果页面加载时商品已经在购物车中,通知会错误触发。
tsx
// ✅ 正确:副作用应放在触发它的事件处理函数中
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 Chains — Consolidate Into Event Handlers

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

Chaining Effects that each set state based on other state creates a cascade of unnecessary re-renders and rigid, fragile code.
tsx
// 🔴 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]);
  // ...
}
tsx
// ✅ 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!');
      }
    }
  }
  // ...
}

多个Effects通过设置state互相触发,会导致不必要的渲染级联,同时让代码变得僵化且脆弱。
tsx
// 🔴 错误:Effects互相触发的链式调用(多达4次渲染!)
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]);
  // ...
}
tsx
// ✅ 正确:尽可能派生值,其余逻辑合并到事件处理函数中
function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // 派生值——无需状态
  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. Data Fetching — Use a Data-Fetching Library

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

Fetching data in
useEffect
is fragile. You must manually handle race conditions, loading states, caching, error handling, and cleanup. Raw
useEffect
fetching has no caching, no deduplication, and no SSR support.
tsx
// 🔴 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]);
  // ...
}
tsx
// ✅ 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.
}
Other good alternatives:
  • Next.js / Remix:
    loader
    functions, Server Components,
    use()
    + Suspense
  • SWR:
    useSWR(key, fetcher)
    — similar to TanStack Query
  • tRPC: End-to-end type-safe fetching with built-in React Query integration
If you must use
useEffect
for fetching (no framework, no library), at minimum:
  1. Add the
    ignore
    cleanup flag to prevent race conditions
  2. Extract the logic into a reusable custom hook (e.g.
    useData(url)
    )
  3. Handle loading, error, and empty states explicitly

useEffect
中手动获取数据是脆弱的。你必须手动处理竞态条件、加载状态、缓存、错误处理和清理逻辑。原生
useEffect
获取数据没有缓存、没有请求去重、不支持SSR
tsx
// 🔴 错误:在useEffect中手动获取数据——存在竞态条件、无缓存、不支持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]);
  // ...
}
tsx
// ✅ 正确:使用TanStack Query(或框架自带的数据原语)
import { useQuery } from '@tanstack/react-query';

function SearchResults({ query }) {
  const { data: results = [], isLoading } = useQuery({
    queryKey: ['search', query],
    queryFn: () => fetchResults(query),
    enabled: !!query,
  });
  // 自动缓存、请求去重、竞态条件处理、
  // 后台重新获取、SSR支持——全部免费获得。
}
其他优秀替代方案:
  • 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. External Store Subscriptions — Use
useSyncExternalStore

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

Subscribing to an external data source (browser API, third-party store, observable) using
useEffect
+
setState
is both boilerplate-heavy and can cause tearing in concurrent mode.
tsx
// 🔴 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>;
}
tsx
// ✅ 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>;
}
For state management libraries, use their built-in React hooks instead of manual subscriptions. See the react-render-performance skill for selector-based patterns with XState, Zustand, Redux, and Nanostores.

使用
useEffect
+
setState
订阅外部数据源(浏览器API、第三方状态库、可观察对象)不仅样板代码多,还可能在并发模式下导致渲染不一致。
tsx
// 🔴 错误:使用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>;
}
tsx
// ✅ 正确:useSyncExternalStore——并发安全、代码更少
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 ? '✅ 在线' : '❌ 离线'}</span>;
}
对于状态管理库,使用其内置的React Hook而非手动订阅。可参考react-render-performance技巧中关于XState、Zustand、Redux和Nanostores的选择器模式。

6. Resetting State on Prop Changes — Use a
key

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

Don't use
useEffect
to reset state when a prop changes. React has a built-in mechanism: the
key
prop.
tsx
// 🔴 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)} />;
}
tsx
// ✅ 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
在props变化时重置状态。React有内置的机制:
key
属性。
tsx
// 🔴 错误:在Effect中重置状态
function EditProfile({ userId }) {
  const [comment, setComment] = useState('');

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

  return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
tsx
// ✅ 正确:使用key重置组件状态
function ProfilePage({ userId }) {
  // 当userId变化时,React会卸载旧的EditProfile并挂载新的实例
  return <EditProfile userId={userId} key={userId} />;
}

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

When
useEffect
IS Correct

什么时候
useEffect
才是正确的选择

Effects are appropriate for synchronizing your component with something outside of React:
Use CaseWhy It's Correct
Integrating a non-React widget (map, chart, video player)Syncing React state → imperative DOM API
Setting up a WebSocket or EventSource connectionExternal system lifecycle tied to component
Measuring DOM layout (
getBoundingClientRect
,
ResizeObserver
)
Reading post-render DOM information
Managing focus or scroll position imperativelyImperative DOM interaction
Connecting to hardware APIs (camera, geolocation)External system with subscribe/cleanup
document.title
or other global side-effects
Syncing React state → browser API
Even for these, consider whether a library already handles it (e.g.,
react-intersection-observer
,
framer-motion
).

Effects适用于将组件与React外部的事物同步:
使用场景合理性说明
集成非React组件(地图、图表、视频播放器)同步React状态 → 命令式DOM API
建立WebSocket或EventSource连接外部系统生命周期与组件绑定
测量DOM布局(
getBoundingClientRect
ResizeObserver
读取渲染后的DOM信息
命令式管理焦点或滚动位置命令式DOM交互
连接硬件API(摄像头、地理位置)带有订阅/清理逻辑的外部系统
document.title
或其他全局副作用
同步React状态 → 浏览器API
即使是这些场景,也可以考虑是否已有成熟库处理(如
react-intersection-observer
framer-motion
)。

Decision Flowchart

决策流程图

Ask yourself these questions before writing
useEffect
:
  1. Can I calculate this from existing props/state? → Derive it during render (or
    useMemo
    ).
  2. Is this a response to a user action? → Put it in the event handler.
  3. Am I fetching data? → Use TanStack Query, SWR, or your framework's data primitive.
  4. Am I subscribing to an external store? → Use
    useSyncExternalStore
    or the library's hook.
  5. Am I resetting state when a prop changes? → Use the
    key
    prop.
  6. Am I syncing with an external system that React doesn't control? → ✅ This is a valid
    useEffect
    .

在编写
useEffect
之前,先问自己这些问题:
  1. 能否通过现有props/state计算得到? → 在渲染时直接派生(或使用
    useMemo
    )。
  2. 这是否是对用户操作的响应? → 放在事件处理函数中。
  3. 我是否在获取数据? → 使用TanStack Query、SWR或框架自带的数据原语。
  4. 我是否在订阅外部存储? → 使用
    useSyncExternalStore
    或对应库的Hook。
  5. 我是否要在props变化时重置状态? → 使用
    key
    属性。
  6. 我是否在与React无法控制的外部系统同步? → ✅ 这是
    useEffect
    的合理使用场景。

Summary Checklist

检查清单

  • No
    useEffect
    that sets state derived from other state/props — calculate during render instead
  • No
    useEffect
    that runs logic in response to user events — use event handlers instead
  • No chains of
    useEffect
    s that trigger each other via state updates — consolidate into event handlers or derived values
  • No raw
    useEffect
    for data fetching — use TanStack Query, SWR, or framework data primitives
  • No
    useEffect
    +
    setState
    for external store subscriptions — use
    useSyncExternalStore
    or library hooks
  • No
    useEffect
    to reset state when props change — use the
    key
    prop
  • Every remaining
    useEffect
    is syncing with a genuine external system (DOM API, WebSocket, third-party widget)
  • 没有通过
    useEffect
    设置可从其他state/props派生的状态——改为在渲染时计算
  • 没有通过
    useEffect
    响应用户事件——改为使用事件处理函数
  • 没有通过state更新互相触发的Effect链式调用——合并到事件处理函数或派生值中
  • 没有使用原生
    useEffect
    进行数据获取——使用TanStack Query、SWR或框架数据原语
  • 没有使用
    useEffect
    +
    setState
    订阅外部存储——使用
    useSyncExternalStore
    或库的Hook
  • 没有使用
    useEffect
    在props变化时重置状态——使用
    key
    属性
  • 剩余的
    useEffect
    均用于与真正的外部系统同步(DOM API、WebSocket、第三方组件)